---
title: "Class maps"
description: "The components are headless; the site's theme gives each one its classes, by part and option value, as Nuxt UI's themes do."
canonical_url: "http://localhost:3000/getting-started/theme/class-maps"
---
# Class maps

> The components are headless; the site's theme gives each one its classes, by part and option value, as Nuxt UI's themes do.

Components are themed like [Nuxt UI](https://ui.nuxt.com/getting-started/theme)'s, with [tailwind-variants](https://www.tailwind-variants.org), in two layers:

- each component's **parts**, in the module (`runtime/theme/<component>.ts`: `base`, the root element, then `title`, `icon`…), with no classes: the components are headless;
- the site's **theme**: a class map per component in the site's theme folder (module option `theme`, relative to the source folder: `app/theme/` by default), named after the component's key in kebab-case (`button-link.ts` for `buttonLink`): the classes of the parts by option value.

## A class map

```ts [app/theme/button-link.ts]
import type { ThemeOf } from '#solstice-ui/utils/ui'

export default {
  slots: {
    base: 'inline-flex items-center gap-2 font-semibold',
    leadingIcon: 'size-4',
    trailingIcon: 'size-4',
  },
  variants: {
    size: { small: 'min-h-9 px-4 text-sm', default: 'min-h-11 px-6' },
    outlined: { true: 'border bg-transparent' },
  },
  defaultVariants: { size: 'default' },
} satisfies ThemeOf<'buttonLink'>
```

- `slots`: the classes of each part, always applied.
- `variants`: by option and value (booleans as `true`/`false`); a string applies to `base`, an object to the parts it names.
- `compoundVariants`: when several options have given values (an array matches any of its values).
- `defaultVariants`: the values of the options a component leaves unset (the defaults of its [look options](http://localhost:3000/getting-started/theme/look-options)).
- `motion`: the component's motion (see [Motion](http://localhost:3000/getting-started/theme/motion)).

An option styled only in `compoundVariants` (a colour with each variant) is listed in `variants` too, with empty classes (`color: { primary: '', … }`): its values type the component's option, as in Nuxt UI.

`satisfies ThemeOf<'buttonLink'>` (from `#solstice-ui/utils/ui`) catches a misspelled part.

## How a component reads it

The module imports every file of the folder by its key into `#build/solstice-ui/theme` (generated, updated in dev when a file is added or removed): there is no index to maintain. A component resolves its classes for its options with `useUi(key, parts, options)` (as Nuxt UI's `tv({ extend: theme, ...appConfig.ui.<component> })(options)`) and puts each part's classes and name on its element:

```vue
<a :class="ui.base()" data-slot="base">
  <span :class="ui.label()" data-slot="label">…</span>
</a>
```

The classes are reactive: Studio's preview restyles a block as soon as an option changes.

## tailwind-variants settings

tailwind-variants' settings are the module option `tv` (like Nuxt UI's `ui.tv`). With tailwind-merge on, give it the theme's own token names, or it merges them away (a `text-lead` size and a `text-muted` colour would conflict):

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  solsticeUi: {
    tv: {
      twMerge: true,
      twMergeConfig: {
        extend: {
          theme: {
            text: ['display', 'title', 'lead', 'body', 'eyebrow'],
            color: ['default', 'muted', 'highlighted', 'primary', 'secondary'],
          },
        },
      },
    },
  },
})
```

## What the theme provides

- **A class map per component** the site uses, in its theme folder: a component without one renders without classes. Whole class names: Tailwind scans the source, never build one from pieces; the site's stylesheet scans the folder (`@source`) if Tailwind doesn't find it by itself.
- **Its stylesheet**: the tokens its class maps use (colours, type, corners…), the base styles of the Markdown in the page content (`[data-page-content]`, on elements without a class or a `data-slot`, so a component's own elements keep their classes), the utilities and keyframes utilities can't express, and motion off under `prefers-reduced-motion`.

The module reads no token of the site's but the page column of the full-width rule (the page frame): `--container-page` and `--spacing-gutter` (1280px and 24px without them). Another look is another theme folder, not new components.

## What stays in the component

Without a theme (`theme: false`, or no folder), a component renders no classes. Structure and behaviour stay in the component: show and hide, the layout its options control (a grid's columns, a card's link), accessibility. Its scoped CSS is plain CSS in px (no tokens, no `rem`: a theme may set another root font size), on its parts' `data-slot` and its options' `data-*` attributes (`data-width="full"`).

Rules a theme may change (a row's width or padding) are in `@layer base`, so the theme's classes, in a later layer, win over them.

## Shared spacing

The shared spacing options of the blocks go through the theme's `spacing` entry (`useSpacing(props)`, `spacing.base()` on the root): `spacingTop` and `spacingBottom` (margins) and `pullUp`, by size. A section's spacing is its padding, in its own class map (`page-section`'s `spacingTop` and `spacingBottom`).


## Sitemap

See the full [sitemap](http://localhost:3000/sitemap.md) for all pages.
