Headless Components

Build custom UI components with full control over styling while preserving accessibility and behavior.

Headless Components

PUI supports three levels of component usage. Use low-level hooks, unstyled components, or fully styled defaults. Your choice, your styles.

Level 1

Low-level Hooks

Direct Floating UI access for maximum control. Build completely custom UIs.

Level 2

Unstyled Components

Component behavior without default styles. Bring your own CSS.

Level 3

Styled Components

Ready-to-use components with Tailwind CSS styling.

CSS Framework Presets

Switch between different CSS framework presets to see how unstyled components adapt.

Current framework: tailwind

Unstyled Button

Use variant='unstyled' and provide your own classes.

<.button variant="unstyled" class="#{@unstyled_button_class}">
Custom Styled Button
</.button>

Unstyled Menu Button

Dropdown behavior with fully custom styling on trigger, menu, and items.

<.menu_button
variant="unstyled"
class="#{@unstyled_button_class}"
content_class="#{@unstyled_menu_class}"
>
Open Custom Menu
<:item class="#{@unstyled_menu_item_class}">
Profile
</:item>
</.menu_button>

Unstyled Dialog

Modal dialog with custom styling for backdrop and content.

<.dialog
id="my-dialog"
variant="unstyled"
class="#{@unstyled_backdrop_class}"
show={@show_dialog}
>
<div class="#{@unstyled_dialog_class}">
<h3>Custom Styled Dialog</h3>
<p>Your content here...</p>
</div>
</.dialog>

Handling Visibility

Unstyled components require visibility classes. Popovers use aria-hidden, dialogs use the hidden attribute.

Popover/Dropdown (aria-hidden)

Popovers toggle aria-hidden attribute. Use aria-hidden:hidden to hide when closed.

content_class="aria-hidden:hidden block bg-white border rounded shadow-lg"

Tooltip (aria-hidden + opacity)

Tooltips combine visibility with opacity transitions for smooth animations.

class="aria-hidden:opacity-0 not-aria-hidden:opacity-100
aria-hidden:pointer-events-none invisible not-aria-hidden:visible"

Dialog (hidden attribute)

Dialogs use the HTML hidden attribute. Use [hidden]:hidden or animation variants.

class="fixed inset-0 bg-black/50 [hidden]:hidden"
# Or with animations:
class="fixed inset-0 bg-black/50 not-[hidden]:animate-in [hidden]:animate-out"

Feature Comparison

Compare the three usage levels to choose the right approach for your use case.

Feature Low-level Hooks Unstyled Styled
Default Styling
ARIA Attributes Manual
Floating UI Integration Direct Built-in Built-in
Custom CSS Override
Best For Custom UIs Design Systems Rapid Prototyping

Code Examples

Example implementations for each usage level.

Level 1: Low-level Hooks

<.popover_base phx-hook="PUI.Popover" data-placement="bottom">
<button class="your-custom-classes">Trigger</button>
<:popup class="your-popup-classes">
Custom content
</:popup>
</.popover_base>

Level 2: Unstyled Components

<.menu_button variant="unstyled" class="btn btn-primary">
Open
<:item class="dropdown-item">Profile</:item>
<:item class="dropdown-item">Settings</:item>
</.menu_button>

Level 3: Styled Components

<.button variant="secondary" size="lg">
Click me
</.button>

<.alert variant="destructive">
<:title>Error</:title>
<:description>Something went wrong.</:description>
</.alert>