Update: Ariakit Styles is in alpha. If you want to try it, join us on Discord (see the #news
channel).
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:
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.
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.
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.
In addition to addressing the issues mentioned above, here are other requirements Ariakit Styles should take into account.
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.
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" />
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" />;
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.
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.
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>
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.
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:
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>
Ariakit Styles will automatically set border and shadow colors when using ak-layer
.
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);
...
}
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
}
...
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.