Component First Strategy
Why zudo-doc uses components with utility classes instead of custom CSS class names.
zudo-doc follows a component-first strategy: always express UI as components with Tailwind utility classes. Never create custom CSS class names with separate stylesheets.
The Problem
When projects use a utility CSS framework alongside a component framework, developers frequently fall back to traditional CSS patterns. Instead of composing utility classes inside components, they create custom CSS class names — .profile-card, .btn-primary, .sidebar-nav — with separate stylesheets or CSS modules.
This creates a fragmented codebase:
Some components use Tailwind utilities inline
Others introduce custom CSS classes with BEM naming or CSS modules
Some mix both approaches in the same file
For AI agents, this is a particularly common failure mode. Given a task like "build a profile card," an agent will often generate a .profile-card class with a CSS module — the pattern seen most often in training data. Over time, the codebase becomes a patchwork of conflicting styling approaches.
The Rule
The component itself is the abstraction. CSS class names like .card or .btn-primary are unnecessary — the component handles encapsulation, and utility classes handle styling.
Need a card? Create a
<Card>component with utility classesNeed a button variant? Create a
<Button variant="primary">componentNeed a layout pattern? Create a
<PageLayout>component
In zudo-doc
zudo-doc uses Preact components (.tsx) on top of zfb, with Tailwind CSS v4 utilities. The same rule applies whether a component renders only on the server or also hydrates on the client:
Server-rendered components
Most UI is server-rendered — zero JavaScript, utility classes inline:
// packages/zudo-doc/src/footer/footer.tsx
export function Footer({ copyright }: Props) {
return (
<footer class="border-t border-muted bg-surface px-hsp-xl py-vsp-xl">
<div
class="text-center text-caption text-muted"
dangerouslySetInnerHTML={{ __html: copyright }}
/>
</footer>
);
}No .footer class. No footer.module.css. The component is the abstraction.
Client-hydrated islands
Interactive components register themselves with zfb's island runtime through their own useEffect / event-binding code, but use the same utility-class approach:
// src/components/sidebar-toggle.tsx
export function SidebarToggle({ label }: Props) {
const [open, setOpen] = useState(false);
return (
<button
class="lg:hidden flex items-center gap-hsp-xs text-fg"
onClick={() => setOpen(!open)}
>
{label}
</button>
);
}Anti-Pattern
Do not create CSS class names in a zudo-doc project:
/* WRONG — don't create custom CSS classes */
.profile-card {
display: flex;
gap: 1rem;
padding: 1.5rem;
}
.profile-card__name {
font-size: 1.25rem;
font-weight: 600;
}// WRONG — custom class names bypass the design system
<div class="profile-card">
<h3 class="profile-card__name">{name}</h3>
</div>Instead:
// RIGHT — utility classes, the component is the abstraction
<div class="flex gap-hsp-md p-hsp-lg">
<h3 class="text-body font-semibold">{name}</h3>
</div>Component Variants via Props
Instead of CSS modifier classes (.btn--primary, .btn--secondary), use component props:
function Button({ variant = "primary", children }) {
const styles = {
primary: "bg-accent text-bg hover:bg-accent-hover",
secondary: "bg-surface text-fg border border-muted",
};
return (
<button className={`${styles[variant]} font-semibold py-vsp-xs px-hsp-md rounded`}>
{children}
</button>
);
}Usage:
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>No .btn-primary class to maintain. The variant prop is type-safe, auto-completable, and self-documenting.
Component Composition
Complex layouts are built by composing smaller components — not by adding more CSS:
<div class="divide-y divide-muted">
{users.map((user) => (
<div class="flex items-center gap-hsp-md py-vsp-sm">
<Avatar src={user.avatar} size="sm" />
<div class="flex-1 min-w-0">
<p class="text-small font-medium text-fg truncate">{user.name}</p>
<p class="text-caption text-muted truncate">{user.email}</p>
</div>
</div>
))}
</div>Each piece — <Avatar>, the list layout — is a component. No .user-list__item or .user-list__avatar class names needed.
Using zudo-doc's Design Tokens
Always use project tokens instead of arbitrary values:
// WRONG — arbitrary values bypass the design system
<div class="p-[1.2rem] text-[0.875rem] text-[#6b7280]">
// RIGHT — use design tokens
<div class="p-hsp-md text-small text-muted">See Design System for available spacing, typography, and color tokens.
When Custom CSS Is Acceptable
The only place custom CSS belongs in zudo-doc is in src/:
Content typography — the
.zd-contentclass styles rendered MDX elements (headings, paragraphs, lists) because these elements are generated by the MDX pipeline, not authored as componentsDesign token definitions — the
@themeblock that registers Tailwind tokens
Everything else — every component, every layout, every UI element — uses utility classes directly.
Rules Summary
Always create components — not CSS classes
Use utility classes directly in component markup
Never create CSS module files or custom class names
Use props for variants — not CSS modifiers
Compose components — build complex UI from smaller components, not from more CSS
Use project tokens —
text-fg,bg-surface,p-hsp-md, not arbitrary values