
Shadcn Code Review
- 183 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Review React UI diffs that use shadcn/ui for component patterns, accessibility, styling consistency, and composition mistakes.
About
The shadcn-code-review skill from existential-birds/beagle specializes PR review for React frontends built with shadcn/ui. It inspects component selection, Radix accessibility patterns, Tailwind class usage, variant props, and composition anti-patterns so SaaS, extension, and content UIs stay consistent before merge.
- Checks shadcn/ui component usage and variants
- Flags accessibility and Radix pattern issues
- Enforces Tailwind and design-token consistency
- Reviews composition and prop misuse
- Targets React SaaS and content frontend PRs
Shadcn Code Review by the numbers
- 183 all-time installs (skills.sh)
- Ranked #888 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill shadcn-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 183 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Review React UI diffs that use shadcn/ui for component patterns, accessibility, styling consistency, and composition mistakes.
Files
shadcn/ui Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| className in CVA, missing VariantProps, compound variants | references/cva-patterns.md |
| asChild without Slot, missing Context, component composition | references/composition.md |
| Missing focus-visible, aria-invalid, disabled states | references/accessibility.md |
| Missing data-slot, incorrect CSS targeting | references/data-slot.md |
Review Checklist
- [ ]
cn()receives className, not CVA variants - [ ]
VariantProps<typeof variants>exported for consumers - [ ] Compound variants used for complex state combinations
- [ ]
asChildpattern uses@radix-ui/react-slot - [ ] Context used for component composition (Card, Accordion, etc.)
- [ ]
focus-visible:states, not just:focus - [ ]
aria-invalid,aria-disabledfor form states - [ ]
disabled:variants for all interactive elements - [ ]
sr-onlyfor screen reader text - [ ]
data-slotattributes for targetable composition parts - [ ] CSS uses
has()selectors for state-based styling - [ ] No direct className overrides of variant styles
Hard gates (before writing findings)
Run these in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
1. Location evidence — Pass: Each issue lists a repo path and either a line range or a short verbatim quote from the file you read (not from memory or diff-only guesswork).
2. Exemption check — Pass: For each issue, you can state in one line why it is not covered by Valid Patterns (Do NOT Flag).
3. Context-sensitive claims — Pass: For accessibility or Radix-related flags, you checked the file for imports/wrappers showing what actually runs (or you cite the concrete gap).
4. Protocol — Pass: You completed the Pre-Report Verification Checklist in review-verification-protocol for this review.
Valid Patterns (Do NOT Flag)
These are correct patterns that should NOT be flagged as issues:
max-h-(--var)- correct Tailwind v4 CSS variable syntax (NOT v3 bracket notation)text-[color:var(--x)]- valid arbitrary value syntax- Copying shadcn component code into project - intended usage pattern
- Not documenting copied shadcn components - library internals, not custom code
- Using cn() with many arguments - composition is the pattern
- Conditional classes in cn() arrays - valid Tailwind pattern
- Extending primitive components without additional docs - well-known base
Context-Sensitive Rules
Apply these rules with appropriate context awareness:
- Flag accessibility issues ONLY IF not handled by Radix primitives underneath
- Flag missing aria labels ONLY IF component isn't using accessible radix primitive
- Flag variant proliferation ONLY IF variants could be composed from existing
- Flag component documentation ONLY IF it's custom code, not copied shadcn
Library Convention Note
shadcn/ui components are designed to be copied and modified. Code review should focus on:
- Custom modifications made to copied components
- Integration with application state/data
- Accessibility in custom usage contexts
Do NOT flag:
- Standard shadcn component internals
- Radix primitive usage patterns
- Default variant implementations
When to Load References
- Reviewing variant definitions → cva-patterns.md
- Reviewing component composition with asChild → composition.md
- Reviewing form components or interactive elements → accessibility.md
- Reviewing multi-part components (Card, Select, etc.) → data-slot.md
Review Questions
1. Are CVA variants properly separated from className props? 2. Does asChild composition work correctly with Slot? 3. Are all accessibility states (focus, invalid, disabled) handled? 4. Are data-slot attributes used for component part targeting? 5. Can consumers extend variants without breaking composition?
Before Submitting Findings
Complete Hard gates (especially gate 4), then report only issues that still pass the review-verification-protocol pre-report checks.
Accessibility Patterns
Critical Anti-Patterns
1. Using :focus Instead of :focus-visible
Problem: Visible focus rings on mouse clicks create poor UX. Use focus-visible for keyboard-only focus.
// BAD - :focus shows ring on click
const buttonVariants = cva(
"rounded focus:ring-2 focus:ring-primary" // Shows ring on mouse click
)
// GOOD - :focus-visible shows ring only for keyboard
const buttonVariants = cva(
"rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
)
// Also apply to inputs:
const inputVariants = cva(
"border rounded px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
)2. Missing aria-invalid for Form States
Problem: Screen readers cannot announce validation errors without aria-invalid.
// BAD - visual error state only
export function Input({ error, className, ...props }) {
return (
<input
className={cn(
"border rounded",
error && "border-red-500", // Visual only
className
)}
{...props}
/>
)
}
// GOOD - aria-invalid with proper error announcement
export function Input({ error, className, ...props }) {
const errorId = React.useId()
return (
<div>
<input
className={cn(
"border rounded focus-visible:ring-2",
error && "border-destructive focus-visible:ring-destructive",
className
)}
aria-invalid={error ? "true" : undefined}
aria-describedby={error ? errorId : undefined}
{...props}
/>
{error && (
<p id={errorId} className="text-sm text-destructive mt-1">
{error}
</p>
)}
</div>
)
}3. Missing Disabled States
Problem: Disabled elements must have both visual and semantic disabled states.
// BAD - CSS only, no semantic disabled
export function Button({ disabled, children }) {
return (
<button className={disabled ? "opacity-50 cursor-not-allowed" : ""}>
{children}
</button>
// Missing disabled attribute and aria-disabled
)
}
// GOOD - semantic + visual disabled
const buttonVariants = cva("rounded px-4 py-2", {
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border hover:bg-accent",
},
},
defaultVariants: { variant: "default" },
})
export function Button({ disabled, variant, className, ...props }) {
return (
<button
className={cn(
buttonVariants({ variant }),
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
className
)}
disabled={disabled}
aria-disabled={disabled}
{...props}
/>
)
}4. Missing Screen Reader Text
Problem: Icon-only buttons or visual indicators need sr-only text for screen readers.
// BAD - icon button with no label
export function CloseButton({ onClick }) {
return (
<button onClick={onClick}>
<X className="h-4 w-4" /> {/* No text for screen readers */}
</button>
)
}
// GOOD - sr-only text for screen readers
export function CloseButton({ onClick }) {
return (
<button
onClick={onClick}
aria-label="Close" // For simple cases
>
<X className="h-4 w-4" />
</button>
)
}
// BETTER - visible text with icon
export function CloseButton({ onClick }) {
return (
<button onClick={onClick}>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
)
}
// For status indicators:
export function Badge({ status, children }) {
return (
<div className="flex items-center gap-2">
<div className={cn(
"h-2 w-2 rounded-full",
status === "online" && "bg-green-500",
status === "offline" && "bg-gray-500"
)} />
<span className="sr-only">{status === "online" ? "Online" : "Offline"}</span>
{children}
</div>
)
}5. Missing Keyboard Navigation
Problem: Interactive custom elements must support keyboard navigation.
// BAD - div with onClick, no keyboard support
export function Card({ onClick, children }) {
return (
<div onClick={onClick} className="cursor-pointer">
{children}
</div>
)
}
// GOOD - proper button with keyboard support
export function Card({ onClick, children, ...props }) {
if (onClick) {
return (
<button
onClick={onClick}
className="text-left w-full"
{...props}
>
{children}
</button>
)
}
return <div {...props}>{children}</div>
}
// For custom interactive elements:
export function Tab({ active, onClick, children }) {
return (
<button
role="tab"
aria-selected={active}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onClick(e)
}
}}
tabIndex={active ? 0 : -1}
className={cn(
"px-4 py-2",
active && "border-b-2 border-primary"
)}
>
{children}
</button>
)
}6. Color as Only Indicator
Problem: Color alone cannot convey state (WCAG 1.4.1).
// BAD - color only for required fields
export function Label({ required, children }) {
return (
<label className={required ? "text-red-500" : ""}>
{children}
</label>
)
}
// GOOD - color + text/icon indicator
export function Label({ required, children }) {
return (
<label>
{children}
{required && (
<>
<span className="text-destructive ml-1" aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</>
)}
</label>
)
}
// For status:
export function Status({ status }) {
const icons = {
success: <Check className="h-4 w-4" />,
error: <X className="h-4 w-4" />,
warning: <AlertTriangle className="h-4 w-4" />,
}
return (
<div className={cn(
"flex items-center gap-2",
status === "success" && "text-green-600",
status === "error" && "text-destructive",
status === "warning" && "text-yellow-600"
)}>
{icons[status]}
<span>{status}</span> {/* Text accompanies color */}
</div>
)
}7. Missing Loading States
Problem: Async actions must indicate loading state for screen readers.
// BAD - visual spinner only
export function Button({ loading, children, ...props }) {
return (
<button {...props}>
{loading ? <Spinner /> : children}
</button>
)
}
// GOOD - aria-busy with announcement
export function Button({ loading, children, ...props }) {
return (
<button
aria-busy={loading}
disabled={loading}
{...props}
>
{loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
{children}
{loading && <span className="sr-only">Loading...</span>}
</button>
)
}Review Questions
1. Are focus-visible styles used instead of focus? 2. Is aria-invalid set for error states with describedby? 3. Do disabled elements have both disabled and aria-disabled? 4. Are icon-only buttons labeled with sr-only text or aria-label? 5. Do custom interactive elements support keyboard navigation? 6. Is state conveyed through more than just color? 7. Are loading states announced with aria-busy?
Component Composition
Critical Anti-Patterns
1. asChild Without Slot
Problem: The asChild pattern requires @radix-ui/react-slot to work correctly.
// BAD - asChild without Slot
export function Button({ asChild, children, ...props }) {
if (asChild) {
return children // WRONG - doesn't merge props
}
return <button {...props}>{children}</button>
}
// Usage breaks:
<Button asChild>
<Link href="/">Home</Link> {/* Link doesn't receive Button's props */}
</Button>
// GOOD - using Slot
import { Slot } from "@radix-ui/react-slot"
export function Button({ asChild, className, variant, size, ...props }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
// Usage works correctly:
<Button asChild variant="outline">
<Link href="/">Home</Link> {/* Link receives variant styles and all props */}
</Button>2. Missing Context for Compound Components
Problem: Component parts cannot communicate state without Context.
// BAD - no context, state passed via props (brittle)
export function Card({ variant, children }) {
return (
<div className={cardVariants({ variant })}>
{React.Children.map(children, child =>
React.cloneElement(child, { variant }) // WRONG - fragile, breaks with fragments
)}
</div>
)
}
export function CardHeader({ variant, children }) {
return <div className={headerVariants({ variant })}>{children}</div>
}
// GOOD - using Context
const CardContext = React.createContext<{ variant?: string }>({})
export function Card({ variant = "default", children, ...props }) {
return (
<CardContext.Provider value={{ variant }}>
<div className={cn(cardVariants({ variant }))} {...props}>
{children}
</div>
</CardContext.Provider>
)
}
export function CardHeader({ className, ...props }) {
const { variant } = React.useContext(CardContext)
return (
<div
className={cn(headerVariants({ variant }), className)}
{...props}
/>
)
}
// Usage is clean:
<Card variant="elevated">
<CardHeader>Title</CardHeader> {/* Automatically gets variant */}
<CardContent>Content</CardContent>
</Card>3. Slot Props Not Merged Correctly
Problem: When using asChild, child props must be merged with component props.
// BAD - props collision
export function Button({ asChild, onClick, ...props }) {
const Comp = asChild ? Slot : "button"
return <Comp onClick={onClick} {...props} /> // Child's onClick is overwritten
}
// GOOD - proper prop merging with composeEventHandlers
import { composeEventHandlers } from "@radix-ui/primitive"
import { Slot } from "@radix-ui/react-slot"
export function Button({ asChild, onClick, ...props }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
{...props}
onClick={composeEventHandlers(onClick, (e) => {
// Component's onClick logic
})}
/>
)
}
// Or use Radix's component approach:
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, onClick, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
onClick={onClick}
{...props}
/>
)
}
)4. Not Forwarding Refs with asChild
Problem: Refs break when using asChild without forwardRef.
// BAD - ref not forwarded
export function Button({ asChild, ...props }) {
const Comp = asChild ? Slot : "button"
return <Comp {...props} /> // ref won't work
}
// Usage breaks:
const ref = useRef()
<Button ref={ref} asChild>
<Link>Home</Link> {/* ref is lost */}
</Button>
// GOOD - forwardRef with asChild
export const Button = React.forwardRef<
HTMLButtonElement,
ButtonProps
>(({ asChild = false, className, variant, size, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
{...props}
/>
)
})
Button.displayName = "Button"5. Polymorphic Components Without Type Safety
Problem: Using 'as' prop without proper TypeScript typing loses type safety.
// BAD - no type safety
export function Text({ as = "p", ...props }) {
const Comp = as
return <Comp {...props} /> // No type checking for Comp-specific props
}
// GOOD - typed polymorphic component
import { ElementType, ComponentPropsWithoutRef } from "react"
type PolymorphicProps<E extends ElementType> = {
as?: E
} & ComponentPropsWithoutRef<E>
export function Text<E extends ElementType = "p">({
as,
className,
...props
}: PolymorphicProps<E>) {
const Comp = as || "p"
return (
<Comp
className={cn("text-base", className)}
{...props}
/>
)
}
// Usage is type-safe:
<Text as="h1" onClick={(e) => {/* e is typed correctly */}}>Title</Text>
<Text as="a" href="/about">Link</Text> {/* href required for 'a' */}6. Overusing React.cloneElement
Problem: cloneElement is fragile and breaks with fragments, context, or complex children.
// BAD - cloneElement everywhere
export function List({ spacing, children }) {
return (
<ul>
{React.Children.map(children, child =>
React.cloneElement(child, { spacing }) // Breaks with fragments, context
)}
</ul>
)
}
// GOOD - use Context
const ListContext = React.createContext({ spacing: "md" })
export function List({ spacing = "md", children, ...props }) {
return (
<ListContext.Provider value={{ spacing }}>
<ul {...props}>{children}</ul>
</ListContext.Provider>
)
}
export function ListItem({ className, ...props }) {
const { spacing } = React.useContext(ListContext)
return (
<li
className={cn(listItemVariants({ spacing }), className)}
{...props}
/>
)
}Review Questions
1. Does asChild use Slot from @radix-ui/react-slot? 2. Are compound components using Context for state sharing? 3. Are refs forwarded with React.forwardRef? 4. Are event handlers composed correctly with asChild? 5. Is React.cloneElement avoided in favor of Context?
CVA Patterns
Critical Anti-Patterns
1. className Passed to CVA Instead of cn()
Problem: CVA variants cannot be overridden by consumers. The className should be passed to cn() after CVA, not as a CVA variant.
// BAD - className in CVA
import { cva } from "class-variance-authority"
const buttonVariants = cva("base-styles", {
variants: {
variant: { default: "bg-primary", destructive: "bg-destructive" },
size: { sm: "h-9", lg: "h-11" },
className: {}, // WRONG - className is not a variant
},
})
export function Button({ variant, size, className }) {
return <button className={buttonVariants({ variant, size })} />
}
// GOOD - className in cn()
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva("base-styles", {
variants: {
variant: { default: "bg-primary", destructive: "bg-destructive" },
size: { sm: "h-9", lg: "h-11" },
},
defaultVariants: {
variant: "default",
size: "default",
},
})
export interface ButtonProps extends VariantProps<typeof buttonVariants> {
className?: string
}
export function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}2. Missing VariantProps Export
Problem: Consumers cannot type-check variant props correctly.
// BAD - no type export
const buttonVariants = cva(...)
export function Button({ variant, size }: { variant?: string, size?: string }) {
return <button className={buttonVariants({ variant, size })} />
}
// GOOD - export VariantProps
import { type VariantProps } from "class-variance-authority"
const buttonVariants = cva(...)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export function Button({ variant, size, className, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}3. Not Using Compound Variants
Problem: Complex state combinations create verbose, repetitive variant definitions.
// BAD - manual combinations
const buttonVariants = cva("rounded font-medium", {
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input bg-background",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
sm: "h-9 px-3 text-xs",
default: "h-10 px-4 py-2",
lg: "h-11 px-8",
},
// Trying to handle all combinations manually - WRONG
variantSize: {
"outline-sm": "border-2", // Don't do this
"ghost-lg": "hover:bg-accent/50",
}
},
})
// GOOD - use compoundVariants
const buttonVariants = cva("rounded font-medium", {
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input bg-background",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
sm: "h-9 px-3 text-xs",
default: "h-10 px-4 py-2",
lg: "h-11 px-8",
},
},
compoundVariants: [
{
variant: "outline",
size: "sm",
class: "border-2",
},
{
variant: "ghost",
size: "lg",
class: "hover:bg-accent/50",
},
],
defaultVariants: {
variant: "default",
size: "default",
},
})4. Hardcoding State Classes Instead of Variants
Problem: State-dependent styling should be variants for consistency and reusability.
// BAD - hardcoded state classes
export function Input({ disabled, invalid, className }) {
return (
<input
className={cn(
"rounded border px-3 py-2",
disabled && "opacity-50 cursor-not-allowed",
invalid && "border-red-500",
className
)}
disabled={disabled}
/>
)
}
// GOOD - state variants
const inputVariants = cva("rounded border px-3 py-2", {
variants: {
state: {
default: "",
invalid: "border-destructive focus-visible:ring-destructive",
disabled: "opacity-50 cursor-not-allowed",
},
},
defaultVariants: {
state: "default",
},
})
export function Input({ disabled, invalid, className, ...props }) {
const state = disabled ? "disabled" : invalid ? "invalid" : "default"
return (
<input
className={cn(inputVariants({ state }), className)}
disabled={disabled}
aria-invalid={invalid}
{...props}
/>
)
}5. Missing defaultVariants
Problem: Component behavior is unpredictable without defaults.
// BAD - no defaults
const buttonVariants = cva("base", {
variants: {
variant: { default: "bg-primary", outline: "border" },
size: { sm: "h-9", lg: "h-11" },
},
// Missing defaultVariants - what happens with <Button />?
})
// GOOD - explicit defaults
const buttonVariants = cva("base", {
variants: {
variant: { default: "bg-primary", outline: "border" },
size: { sm: "h-9", lg: "h-11" },
},
defaultVariants: {
variant: "default",
size: "sm",
},
})Review Questions
1. Is className passed to cn() after CVA variants? 2. Are VariantProps exported for type safety? 3. Are compound variants used for complex state combinations? 4. Are state-dependent styles defined as variants? 5. Are defaultVariants specified for all variant groups?
data-slot Pattern
Critical Anti-Patterns
1. Missing data-slot Attributes
Problem: Component parts cannot be targeted by consumers for custom styling without data-slot.
// BAD - no way to target subcomponents
export function Card({ children, ...props }) {
return (
<div className="border rounded-lg" {...props}>
{children}
</div>
)
}
export function CardHeader({ children, ...props }) {
return (
<div className="p-6" {...props}>
{children}
</div>
)
}
// Consumer cannot style CardHeader inside Card without fragile selectors:
<Card className="[&>div]:bg-red-500"> {/* BRITTLE - breaks if structure changes */}
<CardHeader>Title</CardHeader>
</Card>
// GOOD - data-slot for targetable parts
export function Card({ children, ...props }) {
return (
<div className="border rounded-lg" data-slot="card" {...props}>
{children}
</div>
)
}
export function CardHeader({ children, ...props }) {
return (
<div className="p-6" data-slot="card-header" {...props}>
{children}
</div>
)
}
// Consumer can target with stable selector:
<Card className="[&_[data-slot=card-header]]:bg-red-500">
<CardHeader>Title</CardHeader>
</Card>2. Not Using has() Selectors for State-Based Styling
Problem: Parent styling based on child state requires data-slot + has().
// BAD - manual state prop threading
export function Card({ hasError, children }) {
return (
<div className={cn("border", hasError && "border-red-500")}>
{children}
</div>
)
}
export function CardContent({ error, children }) {
return (
<div>
{error && <p className="text-red-500">{error}</p>}
{children}
</div>
)
}
// Usage is verbose:
const [error, setError] = useState("")
<Card hasError={!!error}>
<CardContent error={error}>...</CardContent>
</Card>
// GOOD - has() selector with data-slot
export function Card({ children, ...props }) {
return (
<div
className="border has-[[data-slot=card-content][data-error]]:border-destructive"
data-slot="card"
{...props}
>
{children}
</div>
)
}
export function CardContent({ error, children, ...props }) {
return (
<div data-slot="card-content" data-error={error ? "" : undefined} {...props}>
{error && (
<p className="text-sm text-destructive" data-slot="card-error">
{error}
</p>
)}
{children}
</div>
)
}
// Usage is clean:
<Card>
<CardContent error={error}>...</CardContent>
</Card>3. Incorrect CSS Targeting Without data-slot
Problem: Targeting by element type or class is fragile and breaks with structural changes.
// BAD - targeting by element type
const selectVariants = cva(
// Targeting trigger button directly - fragile
"[&>button]:flex [&>button]:items-center [&>button]:justify-between",
// Targeting value span - fragile
"[&>button>span]:text-sm [&>button>span]:text-muted-foreground"
)
export function Select({ children }) {
return <div className={selectVariants()}>{children}</div>
}
// GOOD - targeting by data-slot
const selectVariants = cva(
"[&_[data-slot=select-trigger]]:flex [&_[data-slot=select-trigger]]:items-center",
"[&_[data-slot=select-value]]:text-sm [&_[data-slot=select-value]]:text-muted-foreground"
)
export function Select({ children, ...props }) {
return (
<div className={selectVariants()} data-slot="select" {...props}>
{children}
</div>
)
}
export function SelectTrigger({ children, ...props }) {
return (
<button data-slot="select-trigger" {...props}>
{children}
</button>
)
}
export function SelectValue({ children, ...props }) {
return (
<span data-slot="select-value" {...props}>
{children}
</span>
)
}4. data-state Without data-slot
Problem: data-state is useful but needs data-slot for scoped targeting.
// BAD - data-state only, no scoping
export function Accordion({ open, children }) {
return (
<div data-state={open ? "open" : "closed"}>
{children}
</div>
)
}
export function AccordionTrigger({ children }) {
return <button>{children}</button>
}
// Consumer cannot target trigger based on parent state:
// Can't write: [&[data-state=open]_button]:rotate-180
// GOOD - data-slot + data-state
export function Accordion({ open, children, ...props }) {
return (
<div
data-slot="accordion"
data-state={open ? "open" : "closed"}
{...props}
>
{children}
</div>
)
}
export function AccordionTrigger({ children, ...props }) {
return (
<button data-slot="accordion-trigger" {...props}>
{children}
</button>
)
}
// Consumer can target:
<Accordion className="[&[data-state=open]_[data-slot=accordion-trigger]]:rotate-180">
<AccordionTrigger>...</AccordionTrigger>
</Accordion>
// Or use has():
<Accordion className="has-[[data-slot=accordion-trigger][aria-expanded=true]]:bg-accent">5. Nested Component Targeting
Problem: Deeply nested components need data-slot for stable targeting.
// BAD - descendant selectors by element
export function Table({ children }) {
return (
<table className="[&_thead_tr]:border-b [&_tbody_tr]:border-b [&_td]:p-4">
{children}
</table>
)
}
// Breaks if you add divs or other elements in structure
// GOOD - data-slot for all parts
export function Table({ children, ...props }) {
return (
<table
data-slot="table"
className="[&_[data-slot=table-header-row]]:border-b [&_[data-slot=table-row]]:border-b [&_[data-slot=table-cell]]:p-4"
{...props}
>
{children}
</table>
)
}
export function TableHeader({ children, ...props }) {
return (
<thead data-slot="table-header" {...props}>
{children}
</thead>
)
}
export function TableRow({ children, ...props }) {
return (
<tr data-slot="table-row" {...props}>
{children}
</tr>
)
}
export function TableCell({ children, ...props }) {
return (
<td data-slot="table-cell" {...props}>
{children}
</td>
)
}6. Using data-slot for State Instead of data-state
Problem: data-slot is for targeting parts, data-state is for state values.
// BAD - using data-slot for state
export function Tab({ active, children }) {
return (
<button
data-slot={active ? "tab-active" : "tab-inactive"} // WRONG - use data-state
>
{children}
</button>
)
}
// GOOD - data-slot for type, data-state for state
export function Tab({ active, children, ...props }) {
return (
<button
data-slot="tab"
data-state={active ? "active" : "inactive"}
role="tab"
aria-selected={active}
{...props}
>
{children}
</button>
)
}
// Targeting:
<TabList className="[&_[data-slot=tab][data-state=active]]:border-b-2">
<Tab active>...</Tab>
</TabList>Review Questions
1. Do all component parts have data-slot attributes? 2. Are has() selectors used for state-based parent styling? 3. Is CSS targeting using data-slot instead of element types? 4. Are data-state and data-slot used together for stateful components? 5. Can consumers reliably target nested component parts? 6. Is data-slot used for identification and data-state for values?