---
title: Styling
description: Learn how to style Olyx components using CSS and data attributes — zero Tailwind, zero class collisions, full control.
---
## Philosophy
Olyx components are styled with **plain CSS** and **data attributes**. No Tailwind. No utility classes baked in. No `className` overrides wrestling with library internals.
Every component exposes its styling surface through `data-*` attributes — `data-ui`, `data-variant`, `data-mode`, `data-size`, and more. You target them with CSS selectors. You win every time.
**Why this matters:**
- **No specificity wars** — Your styles don't compete with the library's classes.
- **No `!important` hacks** — Layers handle cascade order. You write normal CSS.
- **No class collisions** — Data attributes are namespaced by design. `data-ui="button"` will never collide with your app's `.button` class.
- **Framework agnostic styling** — Works with CSS Modules, vanilla CSS, PostCSS, or any tool that outputs CSS. No preprocessor lock-in.
---
## How It Works
Every Olyx component renders with a `data-ui` attribute that identifies it, plus additional `data-*` attributes for its current state:
```tsx
```
Renders as:
```html
```
We style it by targeting these attributes:
```css
[data-ui="button"] {
/* base styles for all buttons */
}
[data-ui="button"][data-variant="primary"] {
/* primary variant */
}
[data-ui="button"][data-mode="ghost"] {
/* ghost mode */
}
[data-ui="button"][data-size="sm"] {
/* small size */
}
```
---
## Data Attributes vs CSS Classes
Here's a concrete comparison to show why data attributes are a better styling API for component libraries.
### The CSS Class Approach
```tsx
{/* The library ships this: */}
```
```css
/* You want to customize it: */
.btn-primary {
background: hotpink; /* ❌ might not apply — library's .btn-primary might be more specific */
}
/* So you escalate: */
.btn.btn-primary {
background: hotpink; /* ❌ still loses to .btn.btn-primary.btn-filled */
}
/* Nuclear option: */
.btn-primary {
background: hotpink !important; /* 💀 works, but now nothing can override you either */
}
```
**Problems:**
- Class name collisions with your app code
- Specificity depends on how many classes the library stacks
- Overriding requires matching or exceeding their specificity
- `!important` is the only reliable escape hatch (and it's a trap)
### The Data Attribute Approach (Olyx)
```tsx
```
```css
[data-ui="button"][data-variant="primary"] {
background: hotpink; /* ✅ applies cleanly — same specificity as the library */
}
/* Want to scope it further? */
.my-form [data-ui="button"][data-variant="primary"] {
background: hotpink; /* ✅ higher specificity, scoped to your context */
}
```
**Why it works:**
- `[data-ui="button"]` has the same specificity as `.btn` (0,1,0) — level playing field
- No class name pollution in your markup
- No guessing what internal class names the library uses
- Your overrides compose naturally with CSS cascade
### Side-by-Side Specificity
| Selector | Specificity | Notes |
|---|---|---|
| `.btn` | 0,1,0 | Single class |
| `.btn.btn-primary` | 0,2,0 | Two classes — harder to override |
| `.btn.btn-primary.btn-filled` | 0,3,0 | Three classes — good luck |
| `[data-ui="button"]` | 0,1,0 | Single attribute — same as one class |
| `[data-ui="button"][data-variant="primary"]` | 0,2,0 | Predictable, composable |
Data attributes keep specificity **flat and predictable**. You always know exactly what you're competing with.
---
## CSS Layers
Olyx uses [CSS Cascade Layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) to give you explicit control over which styles take priority.
### Layer Order
Defined in globals.css
```css
@layer reset, base, utilities, components, misc, tokens;
```
Layers are applied in the order they're declared — **later layers have higher priority**:
| Layer | Priority | Purpose | What Lives Here |
|---|---|---|---|
| `reset` | 1 (lowest) | Box model reset | `box-sizing: border-box`, zero margins/padding, font-family |
| `base` | 2 | Global defaults | `html`/`body` styles, scroll behavior, scrollbar styling |
| `utilities` | 3 | Helper classes | Reusable utility styles |
| `components` | 4 | Component styles | All `[data-ui="*"]` component CSS |
| `misc` | 5 | Miscellaneous | One-off styles, documentation-specific rules |
| `tokens` | 6 (highest) | Design tokens | Colors, spacing, typography, radii — CSS custom properties |
### Why Layers Matter
Without layers, the only tools CSS gives you for priority control are **source order** and **specificity**. Both are fragile.
With layers:
```css
/* This ALWAYS beats component styles, regardless of specificity or source order */
@layer misc {
[data-ui="button"] {
border-radius: 999px;
}
}
```
```css
/* This NEVER beats component styles — it's in a lower-priority layer */
@layer base {
[data-ui="button"] {
border-radius: 999px; /* ignored for buttons */
}
}
```
### Overriding Component Styles
To override an Olyx component, write your CSS **outside any layer** (unlayered styles always win) or in a higher-priority layer:
```css
/* Option 1: Unlayered — highest priority, always wins */
[data-ui="button"][data-variant="primary"] {
background: var(--my-brand-color);
}
/* Option 2: In a higher layer like misc */
@layer misc {
[data-ui="button"][data-variant="primary"] {
background: var(--my-brand-color);
}
}
```
---
## Common Data Attributes
These are the data attributes used across Olyx components:
| Attribute | Values (Examples) | Purpose |
|---|---|---|
| `data-ui` | `"button"`, `"badge"`, `"dialog-content"` | Identifies the component or sub-component |
| `data-variant` | `"primary"`, `"neutral"`, `"error"` | Color/semantic variant |
| `data-mode` | `"filled"`, `"stroke"`, `"lighter"`, `"ghost"` | Visual treatment |
| `data-size` | `"sm"`, `"md"`, `"lg"` | Size scale |
| `data-shape` | `"rounded"`, `"smoothed"` | Border radius style |
| `data-status` | `"success"`, `"error"`, `"warning"`, `"information"` | Semantic status (Alert, Banner) |
| `data-as-icon` | `"true"`, `"false"` | Icon-only button mode |
| `data-orientation` | `"horizontal"`, `"vertical"` | Layout direction (Tabs, Toolbar) |
### State Attributes (from Base UI)
Components also expose state via attributes from Base UI:
| Attribute | Meaning |
|---|---|
| `data-checked` | Checkbox/Switch is on |
| `data-disabled` | Component is disabled |
| `data-panel-open` | Accordion panel is expanded |
| `data-popup-open` | Popover/Select is open |
| `data-active` | Tab is active |
| `aria-disabled="true"` | Accessibility-aware disabled state |
---
## Internal Custom Properties Pattern
Olyx components use a two-layer custom property system. The **variant** sets scoped tokens, and the **mode** consumes them:
```css
/* Variant defines the palette */
[data-ui="button"][data-variant="primary"] {
--color-button: var(--color-primary);
--color-on-button: var(--color-on-primary);
--color-button-border: var(--color-primary);
--color-button-container: var(--color-primary-container);
}
[data-ui="button"][data-variant="error"] {
--color-button: var(--color-error);
--color-on-button: var(--color-on-error);
--color-button-border: var(--color-error);
--color-button-container: var(--color-error-container);
}
/* Mode consumes the palette */
[data-ui="button"][data-mode="filled"] {
background-color: var(--color-button);
color: var(--color-on-button);
}
[data-ui="button"][data-mode="stroke"] {
border-color: var(--color-button-border);
background-color: var(--color-on-button);
color: var(--color-button);
}
```
This means adding a new variant is just defining four custom properties — the modes already know what to do with them.
---
## Customization Examples
### Override a Specific Variant
```css
/* Make all primary filled buttons use your brand color */
[data-ui="button"][data-variant="primary"] {
--color-button: #6d28d9;
--color-on-button: #fff;
--color-button-container: #ede9fe;
}
```
All modes (`filled`, `stroke`, `lighter`, `ghost`) automatically adapt because they reference these same custom properties.
### Scope Overrides to a Section
```css
.checkout-form [data-ui="button"][data-variant="primary"][data-mode="filled"] {
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
}
```
### Create a New Visual Treatment
```css
/* A "glass" mode for buttons — doesn't exist in Olyx, you add it */
[data-ui="button"][data-mode="glass"] {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
}
```
```tsx
{/* Use data-mode directly in JSX if you need to bypass the prop type */}
```
---
## Style File Structure
Each component ships with two files:
```
packages/react/src/button/
├── index.tsx # Component logic + data attributes
└── style.css # Styles targeting data attributes
```
All component styles live inside `@layer components`:
```css
@layer components {
[data-ui="button"] {
/* ... */
}
}
```
This ensures every component style sits at the same cascade layer — predictable, overridable, no surprises.
---
## Best Practices
- **Use CSS custom properties** for theme-level changes (colors, spacing, radii). Variants already reference them.
- **Override in higher layers or unlayered CSS** to guarantee your styles win without `!important`.
- **Don't fight the cascade** — work with layers, not against them.