An AsheeUI component has no stylesheet of its own: it renders class names, complete, static
strings picked from a dictionary, and the package ships one global stylesheet that tells
Tailwind where those strings live and holds the few rules a utility cannot express. The
application's Tailwind build turns the strings into rules. Everything on this page follows from
that division of labour: what the dictionaries are for, why the stylesheet scans two packages,
and the two rules the test suite enforces.

## Tailwind does the styling

There is no CSS-in-JS runtime, no style object that states a colour or a spacing step, no
`@apply` block compiled ahead of time, and no stylesheet step in the package build: the build
copies the one stylesheet into `dist` and emits JavaScript. What a component chooses is a
string of Tailwind utilities, and the consumer's Tailwind, version 4 or newer, compiles it.

```css
@import "tailwindcss";
@import "@asheeui/web/styles";
```

The order matters, and [Installation](/getting-started/installation) states it once: the
framework's stylesheet extends the Tailwind theme with the colour tokens, so it has to be read
after Tailwind's own import.

Inline `style` is reserved for what only exists once the component runs: a floating list's
position, a layer's z-index, a resizable pane's percentage, an animation's duration, or a
colour a consumer passes as data. The library's own colours, radii, spacing and type are
classes.

## The dictionaries

Every family keeps its class strings in one module named after the component
(`button-styles.ts`, `card-styles.ts`). Most of them live in `@asheeui/core`, beside the config
types and the token maps, because the keys are the design language both renderers answer. The
rest sit with a renderer: the web's `card-styles.ts`, `typography-styles.ts` and layout
dictionaries are in `packages/web`, and the native renderer keeps its own. The rule is one
dictionary per family, not one folder.

Their shape is a record keyed by a token, and the key type is what turns the record into a
checklist:

```ts
export const BUTTON_SIZE_CLASS: Record<Size, string> = {
  sm: "h-8 px-3 text-xs gap-1.5",
  md: "h-9 px-3.5 text-xs sm:h-10 sm:px-4 sm:text-sm gap-2",
  lg: "h-11 px-5 text-sm sm:h-12 sm:px-6 sm:text-base gap-2.5",
};
```

Add a density to `Size` and this file stops compiling until it answers for the new one. A
missing size becomes a type error rather than a `padding: undefined` in a browser. The same
shape covers radii (`Record<Radius, string>`), spacing (`Record<Space, string>`) and every
variant-by-colour matrix (`Record<Variant, Record<ColorRole, string>>`). A family whose maps
are typed more loosely asserts its key set in a test instead.

Where both renderers render a family, the two dictionaries sit side by side and share their
keys: `BUTTON_SIZE_CLASS` for the web and `NATIVE_BUTTON_SIZE_CLASS` for the platform, with
the same `sm`/`md`/`lg` and different strings, because a pointer and a thumb need different
heights. One edit changes the scale for both.

A family may transform a resolved key before it looks it up, a button's `underlined`
treatment resolves the radius to `none`, so a map is keyed by the value the family means, not
always by the raw name the cascade produced.

## Why `cn` is in every render

Component classes are composed with `cn`, which is `clsx` for conditional composition and
`tailwind-merge` for conflicts: when two utilities set the same property, the later one wins.
Every component appends the consumer's `className` last, so a conflict resolves the consumer's
way:

```tsx
<Button radius="md" className="rounded-full">
  Save
</Button>
```

The radius resolves to `md` and the dictionary contributes `rounded-md`; `rounded-full` arrives
last and `twMerge` keeps it. That is the mechanism behind passing `className` to adjust one
instance: without the merge, both rules would be in the class list and the stylesheet's order,
not yours, would decide. `resolveClassKey` covers the other direction, a key a map does not
carry falls back to a named key's class rather than to nothing.

## The `@source` contract

Tailwind generates a utility only when it can read the utility's name as text in a file it has
been told to scan. A library's classes are not in the application's source folder, so the
library has to say where they are. That is what the four lines at the top of the framework
stylesheet do:

| Pattern | What it covers |
| --- | --- |
| `@source "./**/*.{ts,tsx}"` | this package's source, the shape this repository renders from |
| `@source "./**/*.js"` | this package's compiled output, the shape an installed package is |
| `@source "../../core/src/**/*.{ts,tsx}"` | the core package's source |
| `@source "../../core/dist/**/*.js"` | the core package's compiled output |

The paths are relative to the stylesheet, not to the application. A pattern that matches
nothing is ignored, which is why the source shape and the compiled shape can be listed at the
same time.

`@asheeui/core` is on the list because many of the class names live there. The radius and size
vocabularies, the spacing maps and most components' `*-styles.ts` modules are in core, so a
stylesheet that scanned only `./` would generate the web package's classes and not core's. The
failure does not look like a missing stylesheet: the class is on the element, the component
renders, and the padding, gap and corner rounding are simply absent. A control whose label
touches its edges is that bug, not a spacing decision.

A consumer does not write these lines. Importing `@asheeui/web/styles` brings the stylesheet,
and the stylesheet brings its own source list. The obligation is the import: after
`@import "tailwindcss"`, once, at the root. A build that copies the theme block into its own
CSS, or that uses `source(none)` with an explicit list, has to state the patterns against the
installed packages itself.

## Tokens, not literals

The scale lives in `@asheeui/core`'s token module as names and steps rather than as class
strings or CSS values. A step is the Tailwind spacing multiplier: step 4 is `1rem` on the web
and four density-independent pixels on native, which is why the name travels and the unit does
not.

| Token | Step | Gap class |
| --- | --- | --- |
| `none` | 0 | `gap-0` |
| `xs` | 1 | `gap-1` |
| `sm` | 2 | `gap-2` |
| `md` | 4 | `gap-4` |
| `lg` | 6 | `gap-6` |
| `xl` | 8 | `gap-8` |
| `2xl` | 12 | `gap-12` |

Radii work the same way with names instead of steps (`none` through `full`), and a map turns
each name into its class (`rounded-none` through `rounded-full`). The native renderer keeps its
own radius map, because the first two steps map to different class names on its scale; the
token name is the contract and the class is the platform's spelling.

A component therefore resolves a token and looks up a class. It never computes a spacing,
colour or radius value, and it never builds the class from the token, the second half is what
the first scanner is about. Where the scale has no step, the dictionary writes the
arbitrary-value form so the exception is visible in review: `text-[10px]` for a badge's smallest
density, `min-h-[44px]` for a native touch target.
[Layout and spacing](/concepts/layout-and-spacing) states what the spacing tokens mean for a
page.

## The two scanners

`packages/web/src/test/class-conventions.test.ts` walks the web package's source and fails on
either of two findings, and it first asserts that it read more than fifty files so the guard
cannot pass by scanning nothing.

**No dynamically constructed utility names.** The scanner flags a utility root welded to an
interpolation or a concatenation inside a class-name context:

```tsx
<div className={`bg-${colour}-500`} />       // flagged: the name never exists as text
<div className={"rounded-" + radius} />      // flagged: a concatenated name
el.classList.add(`border-${tone}`);          // flagged: a DOM class built at runtime
```

A complete interpolation of a class string that already exists is a lookup rather than
construction, and is accepted:

```tsx
<div className={`${sizeClass} ${toneClass}`} /> // accepted: both strings are complete already
```

So is a template outside a class-name context, such as an id.

The reason is the one the `@source` section states: Tailwind generates what it can read, so a
class assembled at runtime was never generated. Nothing throws; the element renders without
that rule. The dictionary form, one complete string per key, is both the fix and what makes
the map exhaustive.

**No raw palette colours.** The scanner flags a colour utility that names a default-palette
colour, with or without a shade: `bg-red-500`, `border-white/10`, `ring-offset-white`,
`border-x-slate-200`. The only spelling the library accepts is the semantic one,
`bg-primary`, `text-foreground/70`, `border-border`, `bg-danger/10`, because a palette name is
baked in: it ignores the theme and does not follow light to dark.

Both scanners are biased towards missing a case rather than reporting a false one, and the
scanner's own comment says so: the utility roots are a curated list, a class-name context is
recognised by a keyword, and a name built across two lines can slip past. A green run is the
floor rather than a proof. The scanners run over the web package's source; the rule they
enforce applies to every class string in either package.

## The theme variables

The stylesheet's `@theme inline` block maps Tailwind's colour names onto the framework's custom
properties:

```css
@theme inline {
  --color-primary: var(--ashee-primary);
  --color-background: var(--ashee-background);
  /* every colour role, and the scrollbar variables */
}
```

The theme controller writes the `--ashee-*` values for each theme and puts `theme-<name>` on
the document root (`<html>`); the mapping is what makes `bg-primary` mean "the active theme's
primary". The `inline` option matters because each mapped value is itself a variable reference:
the generated utility carries `var(--ashee-primary)` rather than a second variable that points
at it, so one class set serves every theme and switching themes swaps values rather than a
stylesheet.

The `@layer base` block holds the rules that are not one utility: the page's background and
text colour, the scrollbar styling driven by `--ashee-scrollbar-*`, the disabled form field
treatment, the two helpers that hide a scrollbar while keeping the scrolling
(`.scrollable-hidden` and `.scrollbar-hide`), and the keyframes and classes for the modal,
drawer and toast animations. Those are the only rules the library ships as CSS rather than as
a utility, because they are the ones a utility cannot express.

[Theming](/concepts/theming) declares and switches the palettes, and
[Configuration](/concepts/configuration) covers the scrollbar keys.

Styling is deliberately uneventful: a resolved name, one dictionary entry, one static class,
and the consumer's build doing the compile. The [cascade](/concepts/cascade) states where the
name comes from, and [Components](/components) is the index of the families that own a
dictionary.
