Skip to content

Instantly share code, notes, and snippets.

@diegohaz
Last active March 29, 2025 13:56
Show Gist options
  • Save diegohaz/aef9925b1595d83f7e7f0f9e4a9f03ac to your computer and use it in GitHub Desktop.
Save diegohaz/aef9925b1595d83f7e7f0f9e4a9f03ac to your computer and use it in GitHub Desktop.
Ariakit Styles

Update: Ariakit Styles is in alpha. If you want to try it, join us on Discord (see the #news channel).

Ariakit Styles

Motivation

When I started developing Ariakit, I soon recognized that a standalone component library ("Ariakit Components" or "Ariakit React") isn't sufficient for building fully accessible web apps—especially an unstyled one. Even with accessible individual components, it's still possible to create inaccessible web experiences.

Accessibility involves more than isolated component behaviors. The real challenges emerge when combining components, styling them, populating them with content, and testing user interactions. This is why examples ("Ariakit Examples") have become Ariakit's cornerstone feature—they have the potential to demonstrate not just component usage, but also accessibility best practices across entire applications.

While Ariakit Examples currently excel at showcasing Ariakit Components integration (and will eventually include "Third-party Components"), they fall short in styling implementation. Content strategy and testing methodologies also need improvement, though we'll address those separately. Three key issues stand out in our current approach:

Styles are not very portable

You can copy and paste styles into your app. They might work out of the box for a specific case. However, when applying multiple instances, the styles may conflict.

Styles don't serve as a reference

As the project has expanded, the example styles have evolved into more complex and harder-to-read implementations, reducing their effectiveness as references for users creating styles with their preferred CSS frameworks. Consequently, people aren't relying on Ariakit Examples for styling guidance.

Styles don't highlight fundamental issues

Although the examples comply with WCAG standards, this implementation logic is not obivous to users. For instance, users might not recognize that text uses 60% opacity specifically because lower values would fail contrast requirements. This hidden design prevents practical learning opportunities. With Ariakit, developers should absorb accessibility knowledge organically through everyday usage.

Ideal requirements

In addition to addressing the issues mentioned above, here are other requirements Ariakit Styles should take into account.

User customization

It's impossible to create a design system that's accessible for everyone since people perceive colors differently and have unique needs. The only solution is to enable user customization. For most people, this means at least offering a dark/light mode switch and contrast control. Ariakit Styles should support these options without compromising the website's visual consistency.

Tailwind

While a plain CSS solution might seem ideal, it lacks the developer experience provided by CSS frameworks. Tailwind remains the most approachable option, even for newcomers. For skeptics, I'm confident we can build something compelling enough to win them over.

The real challenge with Tailwind lies in reusable styling patterns. The recommended solutions—code repetition or component abstraction—feel suboptimal. Creating new components for shared styles becomes unwieldy quickly, forcing developers into additional abstraction layers like cva, tailwind-merge, or endless prop configurations.

Consider shared :active states for interactive elements: you might start with a Button component, but soon need similar functionality for MenuItem, SelectItem, and Tab. This inevitably leads to creating a Clickable base component. The standard approach with component libraries involves addressing this through polymorphism, which brings its own set of implementation complexities. In Ariakit React, it might look like this:

// ❌ I want to avoid this
<Button render={<Clickable />} />;
<MenuItem render={<Clickable />} />;
<SelectItem render={<Clickable />} />;

But you also want MenuItem and SelectItem to share styles, so you create an Option component for that. Internally, Option renders Clickable with additional styles:

// ❌ I want to avoid this
<Button render={<Clickable />} />;
<MenuItem render={<Option />} />;
<SelectItem render={<Option />} />;

Polymorphism can also combine component behaviors that don't inherently share common styles. For example, a MenuItem could be paired with a MenuButton to trigger a submenu, further complicating things:

// ❌ I want to avoid this
<MenuItem render={<MenuButton render={<Option />} />} />;

Instead, we should style components using CSS utility classes. Ariakit Styles should offer built-in utilities to streamline this approach:

// ✅
<Button className="ak-clickable" />;
<MenuItem className="ak-option" />;
<SelectItem className="ak-option" />;
<MenuItem className="ak-option" render={<MenuButton />} />;

Tailwind's built-in utilities handle overrides by sorting classes based on the number of properties they apply, avoiding the need for tailwind-merge. This approach maintains proper CSS specificity:

// ✅
<Button className="ak-clickable px-4" />

Library and Framework agnostic

Ariakit Styles must remain independent of any specific component library or framework. They should support React, Vue, Solid, or plain HTML while maintaining compatibility with Ariakit React, Radix UI, Headless UI, React Aria—or no library whatsoever.

To ensure cross-library compatibility, Ariakit Styles should avoid relying on library-specific selectors. When developers reference Ariakit React examples to style components from other libraries like React Aria, the library-specific selector should be explicitly visible for easy adaptation.

For instance, we could implement Ariakit React-specific selectors to better assist developers using the library:

// ❌ If ak-option relied on [data-active-item]
<Ariakit.SelectItem className="ak-option" />

However, if developers try applying the same utility to React Aria components, it won't work out of the box, potentially causing confusion:

// ❌ Requires adjustments because React Aria has [data-focused], not [data-active-item]
<ReactAria.ListBoxItem className="ak-option" />

Instead, we should prioritize native CSS states by default and explicitly handle library-specific selectors on examples:

// ✅ Uses native states (:focus-visible, :hover)
// May not work out of the box, but maintains consistent styling across libraries
<Ariakit.SelectItem className="ak-option" />;
<ReactAria.ListBoxItem className="ak-option" />;

// ✅ Library-specific implementation
<Ariakit.SelectItem className="ak-option-idle data-active-item:ak-option-focus" />;
<ReactAria.ListBoxItem className="ak-option-idle data-focused:ak-option-focus" />;

Shadcn

People love Shadcn, so we should provide a migration guide from the beginning. Ideally, we could offer the same components, perhaps one version built with Radix UI and another using Ariakit React, allowing users to switch component libraries while maintaining familiarity with the Shadcn CLI they already know.

Essentials

Ariakit Styles should offer essential low-level utilities installable via npm and importable into Tailwind CSS configurations:

@import "tailwindcss";
@import "@ariakit/tailwind";

@theme {
  --contrast: 0;
  
  --color-canvas: #111;
  --color-primary: #007acc;
  /* ...any other --color-* will be available as a layer color */
}

By defining just a few CSS variables, the entire theme adapts, with variations for text, borders, and more generated automatically.

The layer system

Elements closer to the light source should appear lighter, with the user being the focal point. In UI design, popups must maintain a lighter background than their underlying content across both light and dark modes. This principle forms the foundation of the layer system.

Ariakit Styles will implement this through CSS relative color syntax, dynamically generating color shades based on layer hierarchy:

<html>
  <!-- Sets layer to --color-canvas -->
  <body class="ak-layer-canvas">
    <!-- Lighter canvas color (same as ak-layer-1) -->
    <dialog class="ak-layer">
      <!-- Darker canvas color -->
      <input class="ak-layer-down" />
      <!-- Resets the layer to a one-level lighter version of --color-accent -->
      <!-- On hover, makes it darker/lighter depending on the background -->
      <button class="ak-layer-accent-1 hover:ak-layer-pop">OK</button>
    </dialog>
  </body>
</html>

Custom dark/light variants

There should be ak-dark: and ak-light: variants. Instead of relying on the browser's color scheme, they will match the parent layer's color.

Automatic text color

Ariakit Styles should automatically determine whether to use black or white text based on the background color to ensure sufficient contrast. This is particularly crucial for the layer system, where dynamically changing background colors require automatic text color adaptation:

<!-- May have a white text on idle and switch to black on hover -->
<button class="ak-layer-accent hover:ak-layer-10">Text</button>

ak-text-color can be used as a child element to apply the nearest accessible color from the background. For example, this code displays a light blue button with dark blue text:

<button class="ak-layer-blue-50">
  <span class="ak-text-blue-50">Text</span>
</button>

Ariakit will automatically adjust this color to guarantee at least 4.5:1 contrast ratio with the layer's color:

Screenshot showing six colored boxes with colored text inside.

Automatic text opacity

Alongside text color, opacity should adapt to the background to ensure adequate contrast. While users can use ak-text/50 to set opacity to 50%, Ariakit Styles might automatically adjust it—potentially increasing it when needed—to meet higher contrast ratio requirements:

<div class="ak-layer-2 ak-text/50">
  Text
</div>

Automatic border colors

Ariakit Styles will automatically set border and shadow colors when using ak-layer.

Automatic rounded corners

While not an accessibility feature itself, automatic rounded corners play a crucial role in maintaining visual consistency when users customize spacing and radii. Ariakit Styles should dynamically calculate nested corner radii by factoring in the parent element's radius and padding values:

<!-- p-0 rounded-xl (if not inside another rounded element, same as ak-frame-xl/0) -->
<div class="ak-frame-xl"></div>
<!-- p-container rounded-container (if not inside another rounded element, same as ak-frame-container/container) -->
<div class="ak-frame-container"></div>
<!-- p-0 rounded-container (if not inside another rounded element) -->
<div class="ak-frame-container/0"></div>
<!-- p-dialog rounded-dialog (always, same as ak-frame-force-dialog/dialog) -->
<div class="ak-frame-force-dialog">
  <!-- p-field rounded-[calc(var(--radius-dialog)-var(--padding-dialog))] (rounded-field is ignored) -->
  <div class="ak-frame-field"></div>
  <!-- p-dialog rounded-dialog -m-dialog -->
  <div class="ak-frame-cover"></div>
</div>

The example above assumes the developer defined custom properties for container, dialog, and field, but they are optional:

@theme {
  --radius-container: var(--radius-xl);
  --spacing-container: --spacing(1);
  
  --radius-field: var(--radius-lg);
  --spacing-field: --spacing(2);
  
  ...
}

Higher-level utilities

In addition to core low-level utilities, Ariakit Styles should provide high-level utilities such as ak-clickable, ak-option, ak-tab, and more. These components won’t be part of the @ariakit/tailwind package—instead, users can copy/paste them directly from the website.

Since these utilities may contain site-specific CSS, they should be added to the site’s own CSS file. This method also allows users to adapt them to their specific needs:

@import "tailwindcss";
@import "@ariakit/tailwind";

@theme {
  --color-canvas: #111;
  --color-accent: #007acc;
}

@utility ak-option {
  padding-inline: 1rem;
  @apply
    ak-clickable
    hover:ak-layer-pop
    focus-visible:ak-layer-accent
}

...

Monetization

I often feel the urge to make everything free and open-source, allowing everyone to use and do whatever they want with it. However, my business and life partner always reminds me that we need to eat and pay the bills. I often joke that I can blame her when someone asks why I'm not doing something for free.

All jokes aside, none of this work would be possible without a paid product. Initially, I considered making the entire Ariakit Styles package and higher-level utilities paid. Then, I swung to the opposite extreme, thinking about making everything free and hoping it would attract more people to pay for Ariakit Examples. Ultimately, I found a middle ground. @ariakit/tailwind will be free, while the higher-level utilities, which can be copied from the website, will be part of Ariakit Plus.

Additionally, Ariakit Styles will enable us to create more examples focused on styling rather than Ariakit Components. Some of these examples will be part of Ariakit Plus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment