Hey folks, it's Mira here. As I've been building out NEXT-static (my lightweight Next.js starter/template) and powering NEXT-blog (this very site) with it, I've ditched a lot of shadcn/ui dependencies in favor of simpler, prop-based components. Today, I want to dive into why I think a prop-based approach is superior to the compound/sub-component pattern that shadcn (and underlying libraries like Radix UI) uses—especially for type safety, error prevention, and overall developer experience.
If you're new to this:
<Dialog>, <DialogTrigger>, <DialogContent>, etc., that share state via React Context. It's flexible but can feel magical.Let's break it down with pros, cons, and real code examples.
Compound patterns shine for complex, stateful UIs (like dialogs or tabs) because they let you compose freely. But they introduce risks:
<DialogDescription> before <DialogTitle> might confuse screen readers, or adding extra divs inside <TabsList> could glitch keyboard navigation.Here's a shadcn-style Dialog example (simplified):
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
export function ExampleDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
{/* Your content here */}
</DialogContent>
</Dialog>
)
}
Looks clean, but mess up the order (e.g., swap Title and Description) or add a random <div> in <DialogHeader>, and things can break subtly—focus order wrong, ARIA labels missing, etc. TypeScript won't yell at you compile-time for structural errors.
In NEXT-static, I build components like Card, Button, or even more complex ones (like a simple AlertDialog) as single exports that take props. This makes them:
variant="icon" can require an icon prop and forbid children. Compile-time errors catch misuse early.This approach powers NEXT-blog's compact, custom look—cards feel denser without shadcn's opinionated spacing, and everything's tweakable via props.
Prop-based example for a similar Dialog/Alert (in NEXT-static style, using Tailwind + Radix primitives under the hood for a11y):
import { AlertDialog } from '@/components/ui/alert-dialog'; // My custom prop-based version
export function ExampleAlertDialog() {
return (
<AlertDialog
trigger={<Button variant="outline">Open Dialog</Button>}
title="Are you sure?"
description="This action cannot be undone."
actions={[
<Button key="cancel" variant="ghost" onClick={handleCancel}>Cancel</Button>,
<Button key="confirm" variant="destructive" onClick={handleConfirm}>Delete</Button>
]}
>
{/* Optional extra content as children */}
<p>Additional details here.</p>
</AlertDialog>
);
}
Here, props like title, description, trigger, and actions (an array for buttons) enforce a logical structure. TypeScript can make title required, ensure actions is an array of React nodes, and even infer variants. If you forget trigger, TS errors at compile time. No way to "nest wrong"—it's all props!
In prop-based, you can use discriminated unions for killer inference:
type ButtonProps =
| { variant: 'default' | 'outline' | 'ghost'; icon?: never; } // No icon required
| { variant: 'icon'; icon: React.ReactNode; children?: never; }; // Icon required, no children
function Button(props: ButtonProps) { /* ... */ }
Try <Button variant="icon">Text</Button> → TS error: children not allowed for icon variant. Compound patterns struggle with this level of prop interdependence without extra hoops.
With compounds, I've wasted hours debugging why a menu wouldn't close (wrong context) or why tabs skipped on keyboard (extra wrapper div). In NEXT-static's prop-based world, errors are upfront: invalid prop? TS catches it. Wrong variant combo? Prop types prevent it.
For NEXT-blog, this meant faster iteration—custom cards with props for border, shadow, padding let me dial in compact spacing without overriding shadcn defaults or risking structural breaks.
For ultra-complex things (e.g., a full DataTable with sortable headers), compounds allow deeper composition. But for 80% of UI (buttons, cards, alerts, badges), props win. That's why NEXT-static focuses on prop-based atoms/primitives—you compose them your way.
If you're building your own UI kit, try prop-based first. It's empowering, safe, and scales without the magic. Questions? Hit me up in comments.
Green-hair wisdom out 💚✨
– Mira