Skip to content

Styling

Alpine Headless UI components are completely unstyled by design. They provide behavior, state management, and accessibility features while giving you complete control over styling.

Data Attributes

All components automatically add data attributes to their elements, making it easy to style based on component state.

Common Data Attributes

Every component part receives these attributes:

  • data-scope - Identifies which component the element belongs to (e.g., "collapsible", "dialog")
  • data-part - Identifies the role of the element (e.g., "trigger", "content", "root")
  • data-state - Current state of the component or element (e.g., "open", "closed")
  • data-disabled - Present when the component is disabled

Using Data Attributes in CSS

You can target elements using attribute selectors:

css
/* Style based on state */
[data-state="open"] {
  background-color: #f0f0f0;
}

[data-state="closed"] {
  background-color: white;
}

/* Style disabled elements */
[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Target specific component parts */
[data-scope="collapsible"][data-part="trigger"] {
  padding: 1rem;
  border-radius: 0.5rem;
}

Using Data Attributes with Tailwind CSS

Tailwind's arbitrary variant syntax makes it easy to style based on data attributes:

html
<div x-collapsible>
  <button
    x-collapsible:trigger
    class="
      px-4 py-2 rounded-lg
      data-[state=open]:bg-blue-50
      data-[state=closed]:bg-white
      data-disabled:opacity-50
      data-disabled:cursor-not-allowed
    "
  >
    Toggle
  </button>
  <div x-collapsible:content>
    Content
  </div>
</div>

Styling Patterns

State-based Styling

Use data-state attributes to style elements differently based on their current state:

html
<!-- Collapsible indicator that rotates when open -->
<svg
  x-collapsible:indicator
  class="transition-transform data-[state=open]:rotate-180"
>
  <!-- icon -->
</svg>

<!-- Dialog backdrop that fades in -->
<div
  x-dialog:backdrop
  class="
    transition-opacity
    data-[state=open]:opacity-100
    data-[state=closed]:opacity-0
  "
/>

Disabled States

All interactive components support disabled states:

html
<button
  x-collapsible:trigger
  class="
    px-4 py-2
    hover:bg-gray-100
    data-disabled:opacity-50
    data-disabled:cursor-not-allowed
    data-disabled:hover:bg-white
  "
>
  Toggle
</button>

Focus Styles

Remember to add focus styles for keyboard accessibility:

html
<button
  x-dialog:trigger
  class="
    px-4 py-2
    focus:outline-none
    focus:ring-2
    focus:ring-blue-500
    focus:ring-offset-2
  "
>
  Open Dialog
</button>

Component-Specific Styling

Different components may have additional state attributes. Check each component's documentation for specific data attributes it provides.

Example: Collapsible

html
<div x-collapsible class="border rounded-lg overflow-hidden">
  <button
    x-collapsible:trigger
    class="
      w-full px-4 py-3 text-left
      bg-white hover:bg-gray-50
      data-[state=open]:bg-gray-100
      data-disabled:opacity-60
      transition-colors
    "
  >
    Toggle
  </button>
  <div
    x-collapsible:content
    class="px-4 py-3 bg-gray-50 border-t"
  >
    Content
  </div>
</div>

Example: Dialog

html
<!-- Backdrop with fade -->
<div
  x-dialog:backdrop
  class="
    fixed inset-0 bg-black/50
    transition-opacity duration-200
    data-[state=open]:opacity-100
    data-[state=closed]:opacity-0
  "
/>

<!-- Content with scale animation -->
<div
  x-dialog:content
  class="
    fixed inset-0 flex items-center justify-center
    transition-all duration-200
    data-[state=open]:scale-100
    data-[state=closed]:scale-95
  "
>
  <div class="bg-white rounded-lg p-6 shadow-xl">
    <!-- Dialog content -->
  </div>
</div>

Animation Libraries

Alpine Headless UI works seamlessly with animation libraries:

Alpine.js Transitions

Use Alpine's x-transition directive:

html
<div
  x-collapsible:content
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-start="opacity-0 transform scale-95"
  x-transition:enter-end="opacity-100 transform scale-100"
>
  Content
</div>

Tailwind CSS Animations

Define custom animations in your Tailwind config:

js
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        slideDown: {
          from: { height: 0 },
          to: { height: 'var(--radix-collapsible-content-height)' }
        }
      },
      animation: {
        slideDown: 'slideDown 300ms ease-out'
      }
    }
  }
}
html
<div
  x-collapsible:content
  class="data-[state=open]:animate-slideDown"
>
  Content
</div>

Best Practices

Prefer Data Attributes Over JavaScript State

Instead of using Alpine's reactive bindings for styling, prefer data attributes:

html
<!-- ✅ Good - Uses data attributes -->
<button
  x-collapsible:trigger
  class="data-[state=open]:bg-blue-50"
>
  Toggle
</button>

<!-- ❌ Avoid - Unnecessary reactive binding -->
<button
  x-collapsible:trigger
  :class="{ 'bg-blue-50': $collapsible.open }"
>
  Toggle
</button>

Why? Data attributes are automatically managed by the component and keep your markup cleaner.

Use Transitions for Better UX

Add transitions to state changes for smoother interactions:

html
<button
  class="
    transition-colors duration-200
    data-[state=open]:bg-blue-50
    data-[state=closed]:bg-white
  "
>
  Toggle
</button>

Consider Dark Mode

Use Tailwind's dark mode or CSS custom properties:

html
<button
  class="
    bg-white dark:bg-zinc-900
    hover:bg-gray-50 dark:hover:bg-zinc-800
    data-disabled:opacity-60
  "
>
  Toggle
</button>

Maintain Focus Indicators

Always ensure focus states are visible for accessibility:

html
<button
  class="
    focus:outline-none
    focus:ring-2 focus:ring-blue-500
    focus:ring-offset-2
  "
>
  Button
</button>

See Also

Built with Alpine.js and inspired by Zag.js