Compose components
Compose instead of wrapping everything
Combine focused component parts in application code and avoid building a second rigid abstraction layer over the generated source.
Many shadcn components expose related parts, such as CardHeader, CardContent, and CardFooter. You arrange those parts in application code to match the interface you need. The primitive stays generic. Your product layer adds meaning.
Create application components for repeated product concepts, not one wrapper for every UI primitive. A ProjectCard can compose Card and Button because it represents a real object. A MyButton wrapper that only renames every Button prop usually creates indirection without adding meaning.
Use three levels deliberately. Primitive components own reusable interaction and visual contracts. Compositions arrange primitives for a recurring interface pattern. Domain components accept product data and actions, such as a project and an archive callback. Keeping these jobs distinct makes it clear where a change belongs.
Prop mirroring is a warning sign. If a wrapper accepts every Button prop plus several exceptions, callers must understand two APIs and upgrades must preserve both. A focused domain component can expose project, onOpen, and onArchive because those names describe the product rather than the underlying library.
Add Card first:
npx shadcn@latest add card
Then build a ProjectCard outside components/ui that uses CardHeader for the name, CardContent for the description, and a Button in the footer for archive. Change the underlying Card spacing in components/ui/card.tsx and change the archive wording in ProjectCard separately. Each edit should have one obvious owner and should not require forwarding arbitrary primitive props.
Try this on your own project: add Card, build one domain card for a real entity, and confirm you can change Card padding without touching product copy.
Lesson completed