# Let a Primitive Render as Its Child

**SHOULD** · **ID:** `design-render-as-child` · **Category:** design
**Source:** [Composition Patterns](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-render-as-child

> SHOULD: Give styled primitives an asChild / Slot escape hatch so they can render AS the element they wrap (a Button that is really an <a href>) instead of forcing a fixed tag or nesting interactive elements. Styling stays with the primitive; semantics stay with the child.

Give styled primitives an asChild / Slot escape hatch so a Button can BE a link instead of wrapping one

A `<Button>` that only ever renders a `<button>` forces a bad choice the moment you need it to navigate: either fake a link with an onClick (losing href, middle-click, open-in-new-tab, and the right role) or wrap it in an `<a>` and nest two interactive elements, which is invalid and confuses assistive tech. The `asChild` pattern — Radix's Slot — resolves it by merging the primitive's classes, refs, and event handlers onto the single child you pass, so `<Button asChild><a href>` renders one real anchor that looks and behaves like the button. It keeps styling and semantics in separate hands: the primitive owns the look, the child owns what the element actually IS. This is how shadcn/ui buttons become links, menu items become `<Link>`s, and triggers wrap arbitrary controls without a prop explosion.

## Rule snippet

```tsx
<Button asChild>
  <a href="/pricing">Pricing</a>
</Button>
```

## Bad — do not do this

`design-render-as-child-bad`

```tsx
export function RenderAsChildBad() {
  return (
    <div className="w-full max-w-md space-y-3 py-6">
      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs leading-relaxed text-foreground">
        {`// A "button" that is really a link — no href.
<Button onClick={() => router.push('/pricing')}>
  Pricing
</Button>

// Or the wrap-it fix: nested interactive elements.
<a href="/pricing"><Button>Pricing</Button></a>`}
      </pre>
      <div>
        <button className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground">Pricing</button>
      </div>
      <p className="text-xs text-destructive">
        A styled &lt;button&gt; can’t be a real link: no href means no middle-click, open-in-new-tab, or right-click. Wrapping it in an &lt;a&gt; nests two interactive elements.
      </p>
    </div>
  );
}
```

## Good — do this

`design-render-as-child-good`

```tsx
export function RenderAsChildGood() {
  return (
    <div className="w-full max-w-md space-y-3 py-6">
      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs leading-relaxed text-foreground">
        {`// asChild merges the button's styles/behaviour onto its child.
<Button asChild>
  <a href="/pricing">Pricing</a>
</Button>

// Renders one real <a> that looks and behaves like the button.`}
      </pre>
      <div>
        <a
          href="#example"
          className="inline-block rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        >
          Pricing
        </a>
      </div>
      <p className="text-xs text-success">
        asChild (a Slot) merges the styling and props onto the real &lt;a&gt;: correct link semantics, one element, no nesting.
      </p>
    </div>
  );
}
```

## References

- [Vercel — composition-patterns skill](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
- [Radix — asChild / Slot](https://www.radix-ui.com/primitives/docs/guides/composition)
