Compose components
Work with variants and cn()
Use declared variants for repeated visual states and merge occasional caller classes without creating contradictory style combinations.
Generated components commonly use a variant utility and a cn() helper that combines conditional Tailwind classes and resolves conflicts.
Add a named variant when a visual state is reused and belongs to the component API. Pass className for a local layout adjustment. Do not make every one-off spacing choice a permanent variant. Do not let arbitrary classes erase required focus, disabled, or sizing behavior without review.
Variants work best as a small matrix of stable dimensions such as intent and size. If “compact destructive toolbar button on the dashboard” becomes one variant name, layout and product context have leaked into the primitive. Keep placement with the caller and meaning with the component.
Class merging is convenient because later conflicting utilities can win. That is also the risk. A caller can silently replace padding, foreground color, focus ring, or disabled opacity. Decide which classes are intentionally overridable and test the complete variant matrix after changing the merge order.
Open components/ui/button.tsx and find the variant definition. You will see something like variant: { default: '...', destructive: '...' } paired with size: { default: '...', sm: '...' }. Those keys are the public dimensions callers should use.
<Button variant='destructive' size='sm' className='w-full'>
Delete project
</Button>
The className adds layout (w-full) without inventing a new variant name. Inspect the rendered button in DevTools. Focus ring and disabled opacity should still come from the component defaults unless you deliberately override them.
Inspect the Button variants and write down their public dimensions. Add one restrained success variant, use it twice, and use className only for surrounding layout. Render every size against every intent, including focus and disabled states, then try one conflicting class to see exactly what cn() resolves.
Lesson completed