Skip to content
Primitiv home
Framework
Consumption mode

CONCEPTS

Composition

Four patterns turn up in nearly every Primitiv component. Learning them once means every component page afterwards reads faster.

Rendering as something else

Sometimes you want a button that is really a link. Not a link that looks like a button, an actual <a> that carries the button’s styling and behaviour. Every component takes asChild for this:

<Button asChild>  <Link href="/pricing">See pricing</Link></Button>

The Button renders nothing of its own. It hands its props, classes and behaviour to the child you gave it, and that child is what appears in the page.

This matters more than it looks. It is what lets Primitiv work with your router, your analytics wrapper, or any component you already have, without the library needing to know they exist.

without asChild

<Button>
  <Link href="/x">Go</Link>
</Button>

<button>

<a>

a link inside a button

with asChild

<Button asChild>
  <Link href="/x">Go</Link>
</Button>

<a>

class="primitiv-button"

asChild merges them. It does not nest them.

Who holds the value

Any component with a value can work two ways, and you pick per instance.

Let the component hold it. Pass defaultValue and forget about it. The component tracks its own state and tells you when it changes.

<Tabs defaultValue="account" />

Hold it yourself. Pass value and onValueChange, and the component renders whatever you give it.

<Tabs value={tab} onValueChange={setTab} />

Use the first unless you need the second. You need the second when something outside the component has to change the value, or when the value belongs in a URL or a form you already manage.

Components made of parts

Simple components are one element. Anything with structure is several, and you assemble them:

<Tabs.Root defaultValue="account">  <Tabs.List>    <Tabs.Trigger value="account">Account</Tabs.Trigger>    <Tabs.Trigger value="billing">Billing</Tabs.Trigger>  </Tabs.List>  <Tabs.Content value="account">...</Tabs.Content></Tabs.Root>

That is more typing than a single component taking an array of tabs, and it is deliberate. You can put anything between the parts, style each one, and reorder them, without the library having anticipated it.

The parts share state through React context, so Tabs.Trigger knows which tab is open without you passing anything down.

Part names differ slightly between the two paths, and the docs follow whichever you are reading. Headless gives you one export with parts hanging off it (Tabs.Trigger). The copied styled file gives you flat exports (TabsTrigger).

Styling against state

Components describe their own state in the DOM, as data attributes. An open panel carries data-state="open". A disabled control carries data-disabled. You style against those:

.my-trigger[data-state="open"] .chevron {  transform: rotate(180deg);}

This is why the styled layer needs no JavaScript to know what to look like, and why you can restyle any component without touching its behaviour. Every component page lists the attributes it publishes.