Polymorphic rendering with asChild

When asChild is true, Button renders its child element instead of a native button. This lets you use Button's behavior with any element — a link, a div, or a custom component.

typescript
1<Button asChild>
2 <a href="/dashboard">Go to Dashboard</a>
3</Button>

The child element inherits all Button props: onClick, disabled, aria attributes, and keyboard handling.

Overview

Polymorphic rendering lets a component render as any HTML element or React component while keeping its behavior and styles. Flint UI uses the asChild pattern (popularized by Radix UI) instead of the traditional as prop.

Why asChild over as prop

The as prop approach (<Button as="a">) has TypeScript issues: props from the target element leak into the component's type, and ref forwarding gets messy. asChild avoids this by merging props onto the child element instead.

typescript
1// With asChild — clean, type-safe
2<Button asChild>
3 <a href="/dashboard">Go to Dashboard</a>
4</Button>
5
6// Renders: <a href="/dashboard" class="flint-button">Go to Dashboard</a>

How It Works

When asChild is true, the component does not render its default element. Instead, it clones the single child element and merges its own props (className, event handlers, aria attributes, ref) onto that child.

typescript
1import { Button } from '@flint-ui/core'
2import Link from 'next/link'
3
4// Works with Next.js Link
5<Button asChild tone="primary" size="lg">
6 <Link href="/signup">Get Started</Link>
7</Button>
8
9// Works with React Router Link
10<Button asChild variant="ghost">
11 <RouterLink to="/settings">Settings</RouterLink>
12</Button>
13
14// Works with plain HTML elements
15<Button asChild>
16 <a href="https://github.com/flint-ui" target="_blank" rel="noopener">
17 View on GitHub
18 </a>
19</Button>

Rules

  • asChild expects exactly one child element
  • The child must accept a ref (so no raw strings or fragments)
  • Props are merged: component props win for event handlers (they compose), child props win for everything else
  • className values are concatenated, not replaced

Common Patterns

Navigation buttons

typescript
1<Button asChild variant="outline">
2 <a href="/api/auth/login">Sign In</a>
3</Button>

Icon-only triggers

typescript
1<Button asChild size="icon" variant="ghost">
2 <button aria-label="Close" onClick={onClose}>
3 <XIcon />
4 </button>
5</Button>

Card as link

typescript
1<Card asChild>
2 <a href={post.url} className="block hover:shadow-md transition-shadow">
3 <h3>{post.title}</h3>
4 <p>{post.excerpt}</p>
5 </a>
6</Card>