Interactive primitives
PUI is styled by default. For a hook-managed interaction with application-owned
markup and classes, import that family's .Primitive module. Primitives keep
the ID, ARIA, state, and JavaScript hook contract while contributing no visual
classes.
import PUI.Select.Primitive
<.root id="assignee" class="relative">
<.input id="assignee-value" name="assignee" />
<.trigger id="assignee-trigger" listbox_id="assignee-listbox" class="my-trigger">
<.value placeholder="Choose an assignee" />
</.trigger>
<.content id="assignee-listbox" trigger_id="assignee-trigger" class="my-menu">
<.item value="ada" class="my-option data-[active=true]:bg-accent">Ada</.item>
</.content>
</.root>
Interactive Demo
Select Primitive
What PUI still handles
- ARIA attributes and keyboard navigation
- Select positioning, dismissal, and keyboard behavior
- Only application classes control the appearance
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
<div class="flex flex-wrap items-start gap-4">
<PUI.Select.Primitive.root id="docs-primitive-select" class="relative w-64">
<PUI.Select.Primitive.input
id="docs-primitive-select-value"
name="workspace"
/>
<PUI.Select.Primitive.trigger
id="docs-primitive-select-trigger"
listbox_id="docs-primitive-select-listbox"
class="flex w-full items-center justify-between rounded-lg border border-primary/30 bg-primary/10 px-4 py-2 text-sm font-medium text-primary"
>
<PUI.Select.Primitive.value placeholder="Choose a workspace" />
</PUI.Select.Primitive.trigger>
<PUI.Select.Primitive.content
id="docs-primitive-select-listbox"
trigger_id="docs-primitive-select-trigger"
class="aria-hidden:hidden block min-w-64 rounded-lg border border-border bg-background p-1 shadow-xl"
>
<PUI.Select.Primitive.item
value="design"
class="block rounded-md px-3 py-2 text-sm hover:bg-accent data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
>
Design
</PUI.Select.Primitive.item>
<PUI.Select.Primitive.item
value="engineering"
class="block rounded-md px-3 py-2 text-sm hover:bg-accent data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
>
Engineering
</PUI.Select.Primitive.item>
</PUI.Select.Primitive.content>
</PUI.Select.Primitive.root>
</div>
<div class="rounded-xl border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground">
<p class="font-medium text-foreground">What PUI still handles</p>
<ul class="mt-3 space-y-2">
<li>ARIA attributes and keyboard navigation</li>
<li>Select positioning, dismissal, and keyboard behavior</li>
<li>Only application classes control the appearance</li>
</ul>
</div>
</div>
Dropdown Primitive
When to use this
- Building a custom menu with PUI positioning behavior
- Owning every visual class and menu item layout
- Keeping stable ARIA and keyboard contracts
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
<PUI.Dropdown.Primitive.root
id="docs-primitive-dropdown"
class="w-fit"
placement="bottom-start"
>
<PUI.Dropdown.Primitive.trigger
id="docs-primitive-dropdown-trigger"
controls="docs-primitive-dropdown-menu"
class="inline-flex items-center gap-2 rounded-xl border border-border bg-background px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-accent"
>
<.icon name="hero-code-bracket" class="size-4" /> Open custom menu
</PUI.Dropdown.Primitive.trigger>
<PUI.Dropdown.Primitive.content
id="docs-primitive-dropdown-menu"
class="aria-hidden:hidden block w-56 rounded-xl border border-border bg-background p-1 shadow-xl"
>
<PUI.Dropdown.Primitive.item class="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-accent aria-selected:bg-accent aria-selected:text-accent-foreground">
Profile
</PUI.Dropdown.Primitive.item>
<PUI.Dropdown.Primitive.item class="block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-accent aria-selected:bg-accent aria-selected:text-accent-foreground">
Settings
</PUI.Dropdown.Primitive.item>
</PUI.Dropdown.Primitive.content>
</PUI.Dropdown.Primitive.root>
<div class="rounded-xl border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground">
<p class="font-medium text-foreground">When to use this</p>
<ul class="mt-3 space-y-2">
<li>Building a custom menu with PUI positioning behavior</li>
<li>Owning every visual class and menu item layout</li>
<li>Keeping stable ARIA and keyboard contracts</li>
</ul>
</div>
</div>
Dropdown primitive
<PUI.Dropdown.Primitive.root id="account-menu" placement="bottom-start">
<PUI.Dropdown.Primitive.trigger id="account-menu-trigger" controls="account-menu-content">
Account
</PUI.Dropdown.Primitive.trigger>
<PUI.Dropdown.Primitive.content id="account-menu-content" class="aria-hidden:hidden block my-menu">
<PUI.Dropdown.Primitive.item class="w-full text-left aria-selected:bg-accent aria-selected:text-accent-foreground">Profile</PUI.Dropdown.Primitive.item>
<PUI.Dropdown.Primitive.item class="w-full text-left aria-selected:bg-accent aria-selected:text-accent-foreground">Settings</PUI.Dropdown.Primitive.item>
</PUI.Dropdown.Primitive.content>
</PUI.Dropdown.Primitive.root>
Dialog primitive
<PUI.Dialog.Primitive.root :let={actions} id="delete-project">
<PUI.Dialog.Primitive.backdrop id="delete-project-backdrop" class="fixed inset-0 bg-black/50" />
<PUI.Dialog.Primitive.content id="delete-project-content" class="fixed inset-0 m-auto h-fit w-96 rounded-xl bg-white p-6 shadow-xl">
<h2>Delete project?</h2>
<button type="button" phx-click={actions.hide}>Cancel</button>
</PUI.Dialog.Primitive.content>
</PUI.Dialog.Primitive.root>
Use normal HTML for custom static controls such as buttons, alerts, cards, tables, empty states, and native accordions.
For a Select, keyboard navigation keeps DOM focus on the listbox and marks its
current option with data-active="true"; style that attribute. Dropdown items
are native buttons or links, so give them w-full when each should fill the
menu width and style aria-selected with the same surface used for hover.