Skill: anderskev/shadcn-code-review (version 1.1.1)

--- name: shadcn-code-review description: Reviews shadcn/ui components for CVA patterns, composition with asChild, accessibility states, and data-slot usage. Use when reviewing React components using shadcn/ui, Radix primitives, or Tailwind styling. --- # shadcn/ui Code Review ## Quick Reference | Issue Type | Reference | |------------|-----------| | className in CVA, missing VariantProps, compound variants | [references/cva-patterns.md](references/cva-patterns.md) | | asChild without Slot, missing Context, component composition | [references/composition.md](references/composition.md) | | Missing focus-visible, aria-invalid, disabled states | [references/accessibility.md](references/accessibility.md) | | Missing data-slot, incorrect CSS targeting | [references/data-slot.md](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 - [ ] `asChild` pattern uses `@radix-ui/react-slot` - [ ] Context used for component composition (Card, Accordion, etc.) - [ ] `focus-visible:` states, not just `:focus` - [ ] `aria-invalid`, `aria-disabled` for form states - [ ] `disabled:` variants for all interactive elements - [ ] `sr-only` for screen reader text - [ ] `data-slot` attributes 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)](#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](../review-verification-protocol/SKILL.md) 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](#hard-gates-before-writing-findings) (especially gate 4), then report only issues that still pass the [review-verification-protocol](../review-verification-protocol/SKILL.md) pre-report checks.

path: references/accessibility.md
content: # 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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).

```tsx
// 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.

```tsx
// 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?


path: references/composition.md
content: # Component Composition

## Critical Anti-Patterns

### 1. asChild Without Slot

**Problem**: The asChild pattern requires @radix-ui/react-slot to work correctly.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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?


path: references/cva-patterns.md
content: # 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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?


path: references/data-slot.md
content: # 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.

```tsx
// 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().

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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?


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)