## Overview

Every AdiaUI component follows a consistent CSS architecture built on three principles:

- Scoped isolation via `@scope(tag-ui)` — styles cannot leak in or out
- Token-driven theming via `--{component}-{prop}` custom properties — every visual property is configurable
- Zero-specificity defaults via `:where(:scope)` — tokens can be overridden from outside without specificity wars

This pattern gives you the encapsulation benefits of Shadow DOM (no style leakage) with the flexibility of light DOM (global styles, form participation, CSS inheritance).

## The @scope Pattern

CSS `@scope` limits style rules to a specific subtree. AdiaUI scopes each component to its custom element tag:

@scope (button-ui) { /* Everything in here only applies inside <button-ui> */ :scope { ... } /* the button-ui element itself */ [slot="trailing"] { ... } /* children of button-ui */ }

Unlike Shadow DOM, `@scope` does not create a style boundary — it creates a style scope. CSS custom properties still inherit through the scope boundary, global resets still apply, and form elements participate in their parent form natively.

## :where(:scope) vs :scope

This is the key mechanism that makes component tokens overridable. AdiaUI uses two distinct selectors for two different purposes:

### `:where(:scope)` — Zero-specificity token defaults

Token declarations go in `:where(:scope)`, which has zero specificity. This means any external rule — even a bare element selector — can override these values:

@scope (button-ui) { :where(:scope) { --button-bg: var(--a-bg-muted); --button-fg: var(--a-fg-muted); --button-radius: var(--a-radius); /* ... all component tokens declared here ... */ } }

### `:scope` — Base styles (normal specificity)

Structural styles that consume the tokens go in `:scope`, which has normal specificity. These define the component's layout, transitions, and visual properties by referencing the tokens:

@scope (button-ui) { :scope { display: inline-flex; align-items: center; background: var(--button-bg); /* consumes the token */ color: var(--button-fg); /* consumes the token */ border-radius: var(--button-radius); /* consumes the token */ transition: background var(--a-duration-fast) var(--a-easing); } }

The separation is intentional: `:where(:scope)` sets what the values are, `:scope` sets how they are applied. Because the defaults have zero specificity, consumers can retheme without `!important` or deeply nested selectors.

## Naming Convention

Component tokens follow a strict `--{component}-{property}` naming scheme:

Pattern | Example | Meaning
--- | --- | ---
`--{comp}-bg` | `--button-bg` | Background color
`--{comp}-fg` | `--button-fg` | Foreground / text color
`--{comp}-border` | `--button-border` | Border color
`--{comp}-radius` | `--button-radius` | Border radius
`--{comp}-height` | `--button-height` | Component height
`--{comp}-px` | `--button-px` | Horizontal padding
`--{comp}-gap` | `--button-gap` | Internal gap
`--{comp}-font-size` | `--button-font-size` | Font size
`--{comp}-bg-{variant}` | `--button-bg-primary` | Variant-specific background
`--{comp}-{prop}-{state}` | `--button-bg-hover` | State-specific value

All variant and state tokens are pre-declared in `:where(:scope)`, even if they are only used inside a variant selector. This makes every configurable value discoverable at the top of the file.

## Variant Overrides

Variants work by reassigning the base tokens to their variant-specific counterparts. This keeps the `:scope` base styles unchanged — they always read from `--button-bg`, `--button-fg`, etc. Variants just swap what those tokens resolve to:

/* Variant selectors reassign the base tokens */ :scope[variant="primary"] { --button-bg: var(--button-bg-primary); --button-fg: var(--button-fg-primary); } :scope[variant="outline"] { --button-bg: var(--button-bg-outline); --button-fg: var(--button-fg-outline); --button-border: var(--button-border-outline); } :scope[variant="ghost"] { --button-bg: var(--button-bg-ghost); --button-fg: var(--button-fg-ghost); --button-border: var(--button-border-ghost); }

This "token indirection" approach means hover, focus, and disabled states only need to be written once — they always target the base tokens. Variants just redirect which values flow through.

## Consumer Overrides

Because tokens in `:where(:scope)` have zero specificity, consumers can override any token from outside the component with a simple selector:

/* Override from a parent context */ .my-toolbar button-ui { --button-bg: var(--md-sys-color-primary); --button-fg: var(--md-sys-color-primary-on-primary); --button-radius: var(--a-radius-full); } /* Override a specific instance */ button-ui.big-cta { --button-height: 3rem; --button-px: var(--a-space-6); --button-font-size: var(--a-ui-lg); }

You never need `!important` to override component tokens. Even a type selector like `button-ui { --button-bg: red; }` beats the zero-specificity defaults. This is the entire point of the `:where()` wrapper.

## Annotated Example: button-ui

Here is the complete `button-ui` CSS file, annotated to show how each part of the pattern works together:

@scope (button-ui) { /* ── LAYER 1: Token defaults (zero specificity) ── Every configurable value is declared here. Consumers can override any of these from outside. */ :where(:scope) { /* Base tokens — consumed by :scope styles */ --button-bg: var(--a-bg-muted); --button-fg: var(--a-fg-muted); --button-border: transparent; --button-height: var(--a-size); --button-px: var(--a-ui-px); --button-radius: var(--a-radius); --button-font-size: var(--a-ui-size); --button-font-weight: var(--a-ui-weight); --button-gap: var(--a-space-1); /* Hover state tokens */ --button-bg-hover: var(--md-sys-color-neutral-background); --button-fg-hover: var(--md-sys-color-neutral-on-surface); --button-border-hover: var(--md-sys-color-neutral-on-surface); /* Variant: primary */ --button-bg-primary: var(--md-sys-color-primary); --button-fg-primary: var(--md-sys-color-primary-on-primary); /* Variant: outline */ --button-bg-outline: transparent; --button-fg-outline: var(--md-sys-color-neutral-on-surface); --button-border-outline: var(--a-ui-border); /* Variant: ghost */ --button-bg-ghost: transparent; --button-fg-ghost: var(--md-sys-color-neutral-on-surface-variant); --button-fg-ghost-hover: var(--md-sys-color-neutral-on-surface); --button-bg-ghost-hover: var(--a-bg-hover); --button-border-ghost: transparent; --button-border-ghost-hover: var(--md-sys-color-neutral-outline); /* Variant: danger */ --button-bg-danger: var(--md-sys-color-danger); --button-fg-danger: var(--md-sys-color-danger-on-danger); /* Size: sm */ --button-height-sm: var(--a-ui-size-sm); --button-px-sm: var(--a-space-1); --button-font-size-sm: var(--a-ui-sm); /* Size: lg */ --button-height-lg: var(--a-ui-size-lg); --button-px-lg: var(--a-space-2); --button-font-size-lg: var(--a-ui-lg); /* Disabled state */ --button-bg-disabled: var(--a-ui-bg-disabled); --button-fg-disabled: var(--a-ui-text-disabled); --button-border-disabled: transparent; } /* ── LAYER 2: Base styles (normal specificity) ── These consume the tokens above. They define layout and visual properties that apply to ALL variants. */ :scope { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: var(--button-gap); min-width: var(--button-height); min-height: var(--button-height); padding-inline: var(--button-px); border: 1px solid var(--button-border); border-radius: var(--button-radius); background: var(--button-bg); color: var(--button-fg); font-size: var(--button-font-size); font-weight: var(--button-font-weight); cursor: pointer; transition: background var(--a-duration-fast) var(--a-easing), border-color var(--a-duration-fast) var(--a-easing), color var(--a-duration-fast) var(--a-easing); } :scope:focus-visible { outline: none; box-shadow: var(--a-focus-ring); } /* ── LAYER 3: Variants ── Reassign base tokens to variant-specific values. Base styles don't change — only the token values do. */ :scope[variant="primary"] { --button-bg: var(--button-bg-primary); --button-fg: var(--button-fg-primary); } :scope[variant="outline"] { --button-bg: var(--button-bg-outline); --button-fg: var(--button-fg-outline); --button-border: var(--button-border-outline); } :scope[variant="ghost"] { --button-bg: var(--button-bg-ghost); --button-fg: var(--button-fg-ghost); --button-border: var(--button-border-ghost); } /* ── LAYER 4: States ── Hover, disabled, sizes. Written once, works for all variants. */ :scope:not([disabled]):hover { --button-bg: var(--button-bg-hover); --button-fg: var(--button-fg-hover); --button-border: var(--button-border-hover); } :scope[disabled] { --button-bg: var(--button-bg-disabled); --button-fg: var(--button-fg-disabled); --button-border: var(--button-border-disabled); pointer-events: none; } :scope[size="sm"] { --button-height: var(--button-height-sm); --button-px: var(--button-px-sm); --button-font-size: var(--button-font-size-sm); } }

## Why Not Shadow DOM?

Shadow DOM provides strong style encapsulation, but it comes with significant trade-offs for a design system:

Concern | Shadow DOM | @scope + Tokens
--- | --- | ---
Style isolation | Hard boundary — no styles in or out | Soft boundary — scoped but inheritable
Theming | Requires `::part()` or CSS custom properties piercing the boundary | Standard CSS selectors work; tokens override naturally
Form participation | Requires `ElementInternals` + manual delegation | Native — inputs inside the element are in the form
Global styles | Blocked — resets, typography must be duplicated inside | Inherited — global resets and tokens flow through
Dev tools | Styles hidden in shadow tree | All styles visible in the regular cascade
SSR / progressive | Requires declarative shadow DOM or FOUC | Works with plain HTML — styles apply immediately

## Why Not BEM?

BEM (`.block__element--modifier`) solves naming collisions through convention. AdiaUI's approach solves it through the platform:

Concern | BEM | @scope + Tokens
--- | --- | ---
Isolation | Convention-based (easy to break) | Enforced by `@scope`
Selector length | `.button__icon--large` | `icon-ui` (inside `@scope(button-ui)`)
Theming | Override each BEM class individually | Set a token, all usages update
Variants | New class per variant per element | Attribute selector redirects tokens
Specificity | Flat but requires discipline | Structured: zero for defaults, normal for styles

The component token pattern lets you write clean, minimal selectors inside each component while maintaining full isolation. No naming conventions to memorize, no class-name collisions to debug.

## Writing a New Component

Follow this checklist when creating a new component's CSS:

1. Open with `@scope (my-component-ui) {`
2. Declare all tokens in `:where(:scope) { }` — base values, variants, sizes, states
3. Write base styles in `:scope { }` that consume only your component tokens
4. Add `:scope:focus-visible` with `box-shadow: var(--a-focus-ring)`
5. Write variants as `:scope[variant="..."]` that reassign base tokens
6. Write sizes as `:scope[size="..."]` that reassign height/padding/font tokens
7. Write disabled as `:scope[disabled]`
8. Reference global tokens (`--a-*`) in your token defaults, never in base styles

@scope (my-widget-ui) { :where(:scope) { --widget-bg: var(--a-bg-subtle); --widget-fg: var(--md-sys-color-neutral-on-surface); --widget-radius: var(--a-radius-md); --widget-px: var(--a-space-3); } :scope { display: flex; padding-inline: var(--widget-px); background: var(--widget-bg); color: var(--widget-fg); border-radius: var(--widget-radius); } }