Design System
zudo-doc's tight token strategy — spacing, typography, colors, and more.
zudo-doc uses a tight token strategy — instead of importing the full Tailwind framework, only preflight and utilities are loaded, skipping the default theme entirely. The package then supplies a small, intentional set of design tokens. This page covers the full system: spacing, typography, colors, border radius, breakpoints, and layering.
Tight Token Strategy
Tailwind CSS ships with hundreds of built-in values — colors, spacing scales, font sizes, and more. In a themeable project, these defaults cause problems: hardcoded values ignore theme changes and create inconsistency.
The scaffold's src/ imports only Tailwind's preflight (reset) and utilities (functional classes), then imports the package's token stylesheet. The default Tailwind theme layer is deliberately skipped:
@layer zd-preflight, zd-flow;
@import "tailwindcss/preflight" layer(zd-preflight);
@import "tailwindcss/utilities";
@import "@takazudo/zudo-doc/theme.css";
@import "@takazudo/zudo-doc/safelist.css";
@import "@takazudo/zudo-doc/content.css";
@import "@takazudo/zudo-doc/page-loading.css";
@import "@takazudo/zudo-doc/features.css";
/* Override package defaults here when the project needs to. */
@theme {
/* e.g. --color-accent: oklch(0.6 0.2 250); */
}@takazudo/ ships the default @theme tokens. Its package source, packages/, is the source of truth; the scaffold's later, empty @theme block is an override point. The shipped block opens with --color-*: initial to wipe Tailwind's default palette before adding back project color tokens.
Info
This is the core philosophy: define only what the project needs. Every value in the system is intentional, and accidental use of Tailwind defaults is immediately visible.
What Tailwind vocabulary you have
The tight token strategy does not remove Tailwind's utility syntax. It narrows which token-backed utilities have values. Use the project vocabulary below when writing MDX or components.
| Works | Dead (silently unstyled) |
|---|---|
project tokens: text-caption, bg-accent, px-hsp-*, py-vsp-*, w-icon-*, z-index tiers such as z-modal | numeric spacing: p-4, mt-8, w-64 |
tracking-tight, tracking-normal, tracking-wide, tracking-wider | tracking-tighter, tracking-widest |
rounded, rounded-lg, rounded-full | default type scale: text-sm, text-lg |
static utilities: flex, grid-cols-3, hidden, ... | default palette: bg-blue-500 (reset by design) |
arbitrary values: m-[13.37px], w-[42%] — the full escape hatch | shadow-md (only shadow-lg exists) |
The scaffold scans MDX content, src/components/, src/, and pages/ through its @source coverage and Tailwind's automatic detection. A class used in a location that is not scanned is emitted as unstyled markup, so add an @source directive when you introduce a new source location.
To re-enable a default Tailwind vocabulary, import tailwindcss/theme, or define the specific tokens you need in the project's @theme override block. Prefer a local semantic token when the value is part of the project's design language.
For examples of using this vocabulary in MDX components, see Custom Components.
Spacing
zudo-doc separates spacing into two axes: horizontal (hsp) and vertical (vsp). They serve different purposes in layout — horizontal spacing controls inline rhythm and gutters, while vertical spacing controls content flow and section separation. The vertical scale is stretched at larger sizes to give content more breathing room.
Horizontal Spacing (hsp)
| Token | Value | Example Class |
|---|---|---|
hsp-2xs | 0.125rem (2px) | px-hsp-2xs |
hsp-xs | 0.375rem (6px) | px-hsp-xs |
hsp-sm | 0.5rem (8px) | gap-x-hsp-sm |
hsp-md | 0.75rem (12px) | px-hsp-md |
hsp-lg | 1rem (16px) | px-hsp-lg |
hsp-xl | 1.5rem (24px) | px-hsp-xl |
hsp-2xl | 2rem (32px) | px-hsp-2xl |
Vertical Spacing (vsp)
| Token | Value | Example Class |
|---|---|---|
vsp-3xs | 0.25rem (4px) | py-vsp-3xs |
vsp-2xs | 0.4375rem (7px) | py-vsp-2xs |
vsp-xs | 0.875rem (14px) | py-vsp-xs |
vsp-sm | 1.25rem (20px) | gap-y-vsp-sm |
vsp-md | 1.5rem (24px) | py-vsp-md |
vsp-lg | 1.75rem (28px) | py-vsp-lg |
vsp-xl | 2.5rem (40px) | py-vsp-xl |
vsp-2xl | 3.5rem (56px) | py-vsp-2xl |
Both axes also include 0 (0px) and px (1px) utility values.
<!-- Horizontal padding + vertical padding -->
<div class="px-hsp-lg py-vsp-md">Content with asymmetric spacing</div>
<!-- Grid with dual-axis gaps -->
<div class="grid gap-x-hsp-md gap-y-vsp-lg">Grid items</div>Tip
Notice that hsp-lg is 1rem while vsp-lg is 1.75rem — the vertical axis is stretched at larger sizes. This is intentional — vertical flow needs more room than horizontal rhythm.
Element Sizes
Icon Sizes
| Token | Value | Example Class |
|---|---|---|
icon-xs | 0.75rem (12px) | w-icon-xs h-icon-xs |
icon-sm | 1rem (16px) | w-icon-sm h-icon-sm |
icon-md | 1.25rem (20px) | w-icon-md h-icon-md |
icon-lg | 1.5rem (24px) | w-icon-lg h-icon-lg |
Elevation
Only one shadow token is defined.
| Token | Value | Example Class |
|---|---|---|
lg | 0 10px 15px - | shadow-lg |
Typography
Font Sizes
Each semantic role references an abstract scale step (--text-scale-*); the values below are the resolved sizes.
| Token | Value | Example |
|---|---|---|
micro | 0.75rem / 12px | text-micro |
caption | 0.875rem / 14px | text-caption |
small | 1rem / 16px | text-small |
body | 1.2rem / 19.2px | text-body |
title | 1.4rem / 22.4px | text-title |
heading | 3rem / 48px | text-heading |
display | 3.75rem / 60px | text-display |
Font Weights
| Token | Value | Example |
|---|---|---|
normal | 400 | font-normal |
medium | 500 | font-medium |
semibold | 600 | font-semibold |
bold | 700 | font-bold |
Line Heights
| Token | Value | Example |
|---|---|---|
tight | 1.25 | leading-tight |
snug | 1.375 | leading-snug |
normal | 1.5 | leading-normal |
relaxed | 1.625 | leading-relaxed |
Letter Spacing
Four steps from negative to open. Use tracking-tight on large headings to keep them optically tight; tracking-normal resets to the browser default; tracking-wide/tracking-wider suit labels and small-caps text.
| Token | Value | Example |
|---|---|---|
tight | -0.025em | tracking-tight |
normal | normal | tracking-normal |
wide | 0.05em | tracking-wide |
wider | 0.1em | tracking-wider |
Font Families
| Token | Stack | Example |
|---|---|---|
sans | System sans-serif stack | font-sans |
mono | System monospace stack | font-mono |
<h1 class="text-heading font-bold leading-tight">Page Title</h1>
<p class="text-body font-normal leading-normal">Body text</p>
<code class="text-small font-mono">inline code</code>Border Radius
| Token | Value | Example |
|---|---|---|
DEFAULT | 0.25rem (4px) | rounded |
lg | 0.5rem (8px) | rounded-lg |
full | 9999px | rounded-full |
<button class="rounded bg-accent text-bg">Default radius</button>
<div class="rounded-lg bg-surface">Card with larger radius</div>
<span class="rounded-full bg-muted">Pill badge</span>Breakpoints
| Token | Value | Example |
|---|---|---|
sm | 640px | sm:flex |
lg | 1024px | lg:grid-cols-2 |
xl | 1280px | xl:max-w-5xl |
<div class="px-hsp-sm sm:px-hsp-md lg:px-hsp-lg xl:px-hsp-xl">
Responsive horizontal padding
</div>Z-index Tiers
Use semantic z-index tiers instead of arbitrary stack values.
| Token | Value | Example Class |
|---|---|---|
content | 0 | z-content |
local-1 | 1 | z-local-1 |
local-2 | 2 | z-local-2 |
local-3 | 3 | z-local-3 |
sidebar | 10 | z-sidebar |
toolbar | 20 | z-toolbar |
dropdown | 30 | z-dropdown |
popover | 40 | z-popover |
modal-backdrop | 50 | z-modal-backdrop |
modal | 60 | z-modal |
toast | 70 | z-toast |
tooltip | 80 | z-tooltip |
drag | 90 | z-drag |
Colors
Colors use a three-tier strategy: raw palette values (Tier 1) flow into semantic tokens (Tier 2), which feed into component-scoped tokens (Tier 3). Each tier only references the tier above it, so swapping a color scheme updates the entire site at once.
See the Color reference for full details on the color token system, color schemes, and customization.
Coexisting with another @theme
Tailwind v4 merges every @theme block reached from the stylesheet entry into one variable set. When a key appears more than once, the last declaration wins. CSS reports no error for a duplicate or for the resulting overwrite.
zudo-doc reserves 23 bare --color-* alias names and the --color-zd-* prefix. The bare aliases have these suffixes:
bg, fg, sel-bg, sel-fg, surface, muted, accent, accent-hover, code-bg, code-fg, success, danger, warning, info, overlay, image-overlay-bg, image-overlay-fg, chat-user-bg, chat-user-text, chat-assistant-bg, chat-assistant-text, matched-keyword-bg, matched-keyword-fg.
Other names, such as --color-acme-*, are yours. The package chrome consumes the bare aliases through bg-bg, text-fg, border-muted, var(--color-muted), and similar references. A second design system's @theme that defines any of these reserved names is unsupported: whichever declaration is imported last wins silently. This is separate from intentionally overriding zudo-doc's own theme for your project.
The --color-*: initial guardrail clears every --color-* declaration before it, from any file, not just Tailwind's defaults. Import @takazudo/ before your own @theme block, as the scaffold already does.
If the host stylesheet cannot be reordered after zudo-doc's import, use @takazudo/ in place of theme.css. It has identical tokens and rules, with only the color reset removed. Under zfb's split imports (tailwindcss/preflight + tailwindcss/utilities), the default palette never exists: the variant loses nothing there, and the reset in theme.css is a defensive guardrail. A host that imports tailwindcss/theme (or uses a non-zfb @import "tailwindcss" entry) keeps the whole default palette with the variant; theme.css clears that palette when it is declared before the reset.
The --color-zd-* aliases provide one namespaced counterpart per bare alias, with the same default value. Another design system can consume zudo-doc colors through bg-zd-*, text-zd-*, and border-zd-* utilities without using the bare tier. The aliases are declared in @theme static, so their variables are always present in :root, even when your Tailwind scan uses none of them. The utilities themselves are generated only for classes your own scan sees.
For example, give the host design system an acme namespace and import its tokens after zudo-doc:
@theme {
--color-acme-bg: oklch(0.97 0.01 250);
--color-acme-fg: oklch(0.25 0.02 250);
}@layer zd-preflight, zd-flow;
@import "tailwindcss/preflight" layer(zd-preflight);
@import "tailwindcss/utilities";
@import "@takazudo/zudo-doc/theme.css";
@import "./consumer.css";
@import "@takazudo/zudo-doc/safelist.css";
@import "@takazudo/zudo-doc/content.css";
@import "@takazudo/zudo-doc/page-loading.css";
@import "@takazudo/zudo-doc/features.css";What you get with this entry:
| Surface | Result |
|---|---|
--color-acme-bg, --color-acme-fg | Survive the reset because they are declared after it |
| zudo-doc's bare aliases | Stay intact; acme defines no reserved names |
bg-zd-accent | Available to acme components when the class is found by your Tailwind scan |
| Tailwind's default palette | Absent; the entry uses only the split imports |
Keep the two spellings distinct: the raw --zd-* tier is prefix-first, outside @theme, and injected by ColorSchemeProvider; the semantic --color-zd-* tier is category-first, inside @theme. Never insert color- after the raw --zd- prefix: that third spelling connects to neither tier. Check the built CSS as well as the source to confirm that the utilities you use were emitted and reference the intended tokens.
Moving the package chrome to the namespaced tier is explicitly outside this contract. Package and showcase code keep using the bare tier; a host design system that also defines the bare names remains unsupported, including with theme-no-reset.css.
Usage Rules
Tailwind's default theme is disabled
The default Tailwind theme is not imported. Project tokens come from @takazudo/, then the project's @theme override block. A token that is not defined produces unstyled markup.
When another design system shares the entry, follow the import order and reserved-name rules in Coexisting with another @theme.
Do
<!-- Semantic color tokens -->
<p class="text-fg">Primary text</p>
<div class="bg-surface border border-muted">Panel</div>
<a class="text-accent hover:text-accent-hover">Link</a>
<!-- Spacing tokens -->
<div class="px-hsp-lg py-vsp-md">Proper spacing</div>
<div class="gap-x-hsp-sm gap-y-vsp-md">Grid gaps</div>
<!-- Typography tokens -->
<h2 class="text-title font-semibold leading-tight">Heading</h2>Don't
<!-- DON'T: Tailwind defaults — not defined and produce nothing -->
<div class="p-4 bg-gray-500 text-sm">Broken</div>
<!-- DON'T: Hardcoded hex — breaks theming -->
<div class="bg-[#1e1e2e] text-[#f8f8f2]">Breaks on theme switch</div>Default tokens ship from @takazudo/; packages/ is their source of truth. The scaffold's src/ imports those defaults and provides the project override point.
Interaction Rules
Hover-state underline for link-like elements
Elements that navigate (rendered as <a href> or behave as a link) must show an underline on hover and on keyboard focus. Buttons, toggles, and controls do not — they use a border/background change instead.
Do (navigational links):
<a class="text-accent hover:underline focus-visible:underline">Link</a>
<a class="text-fg hover:text-accent hover:underline focus-visible:underline">Sidebar item</a>Don't (missing focus-visible parity):
<!-- DON'T: mouse users get an underline, keyboard users don't -->
<a class="text-fg hover:underline">Inaccessible to keyboard focus</a>Don't (controls):
<!-- DON'T: buttons use border/bg hover, not underline -->
<button class="hover:underline">Use hover:bg-accent/10 instead</button>The pair hover:underline focus-visible:underline is the canonical form — always add both, never one without the other. Precedents: packages/, src/, packages/.
For the broader reasoning (light-mode / dark-mode contrast, when an underline is sufficient vs when a color shift is also needed), consult the / skill locally — its light-mode/dark-mode and three-tier token strategy sections cover the trade-offs.