ShadCN Primitives + Product Wrappers
Intent
Keep ShadCN primitives stable and token-normalized in src/components/ui, and express product typography/semantics in wrappers in src/components/typography or src/components/elements.
Touch vs wrap
Touch the primitive when:
- •It uses non-system tokens (
text-sm,rounded-md,gap-4, default ShadCN colors). - •It is reused widely and needs consistent defaults.
- •Fixing it once prevents repeated call-site overrides.
Wrap the primitive when:
- •You need semantic intent (eyebrow, label, badge copy, product defaults).
- •Typography should be driven by
TextortextVariants. - •The component is a composed pattern (Badge + Text, Button + Icon + Label).
Workflow
- •Import the ShadCN component into
src/components/ui. - •Normalize tokens using
tailwind-shadcn-adaptation(typography/spacing/radius) and class sorting. - •Keep primitives agnostic: layout, shape, state, interaction only. No product semantics.
- •Add a wrapper in
src/components/typographyorsrc/components/elements:- •Compose the primitive with
TextortextVariants. - •Use
asChild/Slot patterns to avoid coupling. - •Provide opinionated defaults for product usage.
- •Compose the primitive with
Cross-skill handoff
- •If you're adapting a new primitive, read
tailwind-shadcn-adaptationfirst. - •If you see repeated typography overrides in call sites, create a wrapper here.
Anti-patterns
- •Adding typography intent props to
uiprimitives. - •Fixing typography in multiple call sites instead of a wrapper.
- •Mixing product semantics into
src/components/ui.
Example
- •
Badgenormalized once insrc/components/ui/badge.tsx. - •
EyebrowBadgeinsrc/components/typographycomposesBadge+Text intent="eyebrow".