diff --git a/.changeset/docs-markdown-tables.md b/.changeset/docs-markdown-tables.md
new file mode 100644
index 000000000..b071001e3
--- /dev/null
+++ b/.changeset/docs-markdown-tables.md
@@ -0,0 +1,5 @@
+---
+"@tabler/docs": patch
+---
+
+Updated markdown tables in the docs with a bordered, scrollable `.docs-table` box and a tighter header row.
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 095b79f09..5e92ac37d 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -4,6 +4,7 @@ import vercel from '@astrojs/vercel'
import mdx from '@astrojs/mdx'
import { satteri } from '@astrojs/markdown-satteri'
import { unwrapJsxParagraphs } from './lib/satteri-unwrap-jsx-paragraphs.mjs'
+import { wrapTables } from './lib/satteri-wrap-tables.mjs'
import { fileURLToPath } from 'node:url'
import { copyAssets } from '../.build/copy-assets'
import { redirects } from './lib/redirects.ts'
@@ -111,7 +112,11 @@ export default defineConfig({
],
markdown: {
// No typographic quote rewriting.
- processor: satteri({ features: { smartPunctuation: false }, mdastPlugins: [unwrapJsxParagraphs] }),
+ processor: satteri({
+ features: { smartPunctuation: false },
+ mdastPlugins: [unwrapJsxParagraphs],
+ hastPlugins: [wrapTables],
+ }),
shikiConfig: {
theme: 'github-dark',
},
diff --git a/docs/lib/satteri-wrap-tables.mjs b/docs/lib/satteri-wrap-tables.mjs
new file mode 100644
index 000000000..db562bd1f
--- /dev/null
+++ b/docs/lib/satteri-wrap-tables.mjs
@@ -0,0 +1,23 @@
+// A markdown table is only as wide as the prose column, so an options table
+// with four columns either squeezes its description or, on a phone, pushes the
+// whole page sideways. Every docs system (Starlight, VitePress, GitHub) puts
+// the table in a scrolling box instead; this does the same with a wrapper
+// element, which keeps the `
` semantics that `display: block` on the
+// table itself would throw away. Only markdown tables are elements here —
+// literal `` markup inside an is JSX and is left alone.
+export function wrapTables() {
+ return {
+ name: 'wrap-tables',
+ element: {
+ filter: ['table'],
+ visit(node, ctx) {
+ ctx.wrapNode(node, {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: ['docs-table'] },
+ children: [],
+ })
+ },
+ },
+ }
+}
diff --git a/docs/scss/docs.scss b/docs/scss/docs.scss
index a1fa8cb89..c97b8a81b 100644
--- a/docs/scss/docs.scss
+++ b/docs/scss/docs.scss
@@ -316,7 +316,7 @@ header > .navbar {
// Prose links keep the primary color but gain a quiet underline, so they
// are findable when scanning and do not rely on color alone (WCAG 1.4.1).
- > :is(p, ul, ol, table) a:not(.btn) {
+ > :is(p, ul, ol, .docs-table) a:not(.btn) {
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--tblr-primary) 40%, transparent);
text-underline-offset: 3px;
@@ -326,6 +326,83 @@ header > .navbar {
}
}
+ // Markdown tables. The wrap-tables hast plugin (lib/satteri-wrap-tables.mjs)
+ // puts each one in a .docs-table box, so a wide table scrolls inside the
+ // column instead of pushing the page out. The wrapper also takes the table
+ // out of core's `.prose > table` rule, so the whole look is defined here:
+ // a bordered grid with a tinted header row (as on GitHub and VitePress),
+ // full width, tighter rows, and a first column that never wraps — the
+ // option or method name is what the reader scans for, and the description
+ // gets whatever is left. The border sits on the box, not the table, so the
+ // rounded corners survive a sideways scroll.
+ > .docs-table {
+ margin-bottom: 1.5rem;
+ overflow-x: auto;
+ border: 1px solid var(--tblr-border-color);
+ border-radius: var(--tblr-border-radius);
+
+ > table {
+ width: 100%;
+ margin: 0;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-collapse: collapse;
+ }
+
+ th,
+ td {
+ padding: 0.5rem 0.75rem;
+ text-align: start;
+ vertical-align: top;
+ border: 1px solid var(--tblr-border-color);
+ }
+
+ // The box already draws the outer edge.
+ tr > :first-child {
+ border-inline-start: 0;
+ }
+
+ tr > :last-child {
+ border-inline-end: 0;
+ }
+
+ thead tr:first-child > * {
+ border-top: 0;
+ }
+
+ tbody tr:last-child > * {
+ border-bottom: 0;
+ }
+
+ th {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-size: 0.8125rem;
+ font-weight: var(--tblr-font-weight-semibold);
+ color: var(--tblr-body-color);
+ white-space: nowrap;
+ background: var(--tblr-bg-surface-tertiary);
+ }
+
+ // Names, types and defaults stay on one line; only the last column wraps.
+ // Every middle cell in the docs is a short label, so the browser's even
+ // split would only spread wrapped fragments across the row.
+ td:not(:last-child) {
+ white-space: nowrap;
+ }
+
+ // On a phone the box scrolls sideways; without a floor the description
+ // column would be the one squeezed into a tall sliver instead.
+ th:last-child,
+ td:last-child {
+ min-width: 18rem;
+ }
+
+ td > p:last-child {
+ margin-bottom: 0;
+ }
+ }
+
// `pretty` only optimises the tail of a block, so it stays cheap on long
// text; `balance` evens out all lines and suits short headings.
> :is(p, li, blockquote, dd),