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.
Low-level Hooks
Direct Floating UI access for maximum control. Build completely custom UIs.
Unstyled Components
Component behavior without default styles. Bring your own CSS.
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.
Custom Styled Dialog
This dialog uses the unstyled variant with custom classes for both the 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>