# Styling

> The three styling modes, granular entry points, cascade layers, and the absence of a Tailwind or CSS-in-JS dependency.

Page: https://svelte-lean.vercel.app/styling · Markdown: https://svelte-lean.vercel.app/styling.md · Index: https://svelte-lean.vercel.app/llms.txt

@svelte-lean/styles is optional CSS for the primitives: design tokens and one stylesheet per primitive, with no JavaScript, no build step and no dependency.

## Three modes

Behavior reads and writes attributes (`aria-selected`, `hidden`), never class names, so it works the same in every mode.

| Mode | You write | From the package |
| --- | --- | --- |
| 1 · Your own CSS | Selectors on `data-slean`, ARIA and native state | Nothing |
| 2 · Package CSS | Imports, token overrides | Tokens, base, one file per primitive |
| [3 · Owned styles](https://svelte-lean.vercel.app/styling/owned-styles.md) | A copied stylesheet you edit | Tokens; behavior stays in the package |

### Mode 1: your own CSS

`app.css`

```css
/* Mode 1: your own CSS. Target the protocol attributes and platform state directly;
 * nothing from @svelte-lean/styles is required. */
[data-slean='button'] {
	padding: 0.5rem 1rem;
	border: 1px solid currentColor;
	border-radius: 4px;
}

[data-slean='button'][data-variant='outline'] {
	background: transparent;
}

[data-slean='tabs'] [role='tab'][aria-selected='true'] {
	border-bottom: 2px solid currentColor;
}

[data-slean='dialog']::backdrop {
	background: rgb(0 0 0 / 0.4);
}
```

### Mode 2: package CSS

The default, and what this site uses. Import only the files your markup needs.

`+layout.svelte`

```ts
// Mode 2: the package, one file per primitive. tokens.css and base.css are required by every
// primitive file; import only the primitives your markup uses. A file that builds on another
// imports it itself: menu.css brings popover.css, select.css brings control.css.
import '@svelte-lean/styles/tokens.css';
import '@svelte-lean/styles/base.css';
import '@svelte-lean/styles/button.css';
import '@svelte-lean/styles/dialog.css';
import '@svelte-lean/styles/menu.css';
```

`+layout.svelte`

```ts
// Everything, for a prototype or when most primitives are in use. Not the primary path.
import '@svelte-lean/styles';
```

## Entry points

- Every primitive file needs `tokens.css` and `base.css`; a file that builds on another imports it itself (Imports column).
- A file imported twice is harmless: a bundler includes it once, and otherwise the copy sits in an earlier layer.

| Import | Contents | Imports | gzip | brotli |
| --- | --- | --- | --- | --- |
| `@svelte-lean/styles/index.css` | Everything below, each file once (convenience entry)(measured with every import resolved) | – | 31124 B | 24738 B |
| `@svelte-lean/styles/tokens.css` | --slean-\* custom properties: light and dark palettes, aliases, forced colors, reduced motion, coarse pointer | – | 4041 B | 3369 B |
| `@svelte-lean/styles/base.css` | Layer order, a reset scoped to \[data-slean\] elements, the shared focus ring, icon sizing | – | 673 B | 563 B |

One file per primitive (51 files)

| Import | Contents | Imports | gzip | brotli |
| --- | --- | --- | --- | --- |
| `@svelte-lean/styles/button.css` | button (Tier 0) | – | 1032 B | 871 B |
| `@svelte-lean/styles/dialog.css` | dialog (Tier 0, native \<dialog\>), in three shapes (ADR 0011 §1.4): centered by default; a drawer with data-slean-side="right\|left\|top\|bottom" (physical edges); an alert dialog with role="alertdialog" (narrower, the question and its answers) | – | 1799 B | 1537 B |
| `@svelte-lean/styles/popover.css` | popover (Tier 0, native \[popover\]) | – | 1011 B | 845 B |
| `@svelte-lean/styles/checkbox.css` | checkbox (Tier 0, native \<input type="checkbox"\>), ADR 0011 §1.11 | – | 1192 B | 1001 B |
| `@svelte-lean/styles/checkbox-group.css` | checkbox group (Tier 1; the select-all box comes from @svelte-lean/primitives/checkbox-group) | – | 460 B | 364 B |
| `@svelte-lean/styles/radio-group.css` | radio group (Tier 0, native \<input type="radio"\> sharing a name) | – | 1860 B | 1615 B |
| `@svelte-lean/styles/tabs.css` | tabs (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tabs) | – | 915 B | 745 B |
| `@svelte-lean/styles/menu.css` | menu (Tier 1; roles, keyboard and activation come from @svelte-lean/primitives/menu) | `popover.css` | 1192 B | 993 B |
| `@svelte-lean/styles/listbox.css` | listbox (Tier 1; keyboard behavior comes from @svelte-lean/primitives/listbox) | – | 923 B | 747 B |
| `@svelte-lean/styles/control.css` | control shell (ADR 0007) | – | 2736 B | 2383 B |
| `@svelte-lean/styles/calendar.css` | calendar (Tier 1, role="grid" of day buttons) | – | 1719 B | 1486 B |
| `@svelte-lean/styles/date-picker.css` | date picker (Tier 2; behavior comes from @svelte-lean/primitives/date-picker) | `control.css`,`calendar.css` | 1417 B | 1225 B |
| `@svelte-lean/styles/input.css` | input (ADR 0011 §1.9) | – | 2887 B | 2527 B |
| `@svelte-lean/styles/field.css` | field (Tier 0: a layout for a label, a control, a description and an error) | – | 703 B | 590 B |
| `@svelte-lean/styles/select.css` | select | `control.css` | 2395 B | 2105 B |
| `@svelte-lean/styles/rating.css` | rating (Tier 0, native \<input type="radio"\> sharing a name) | – | 991 B | 794 B |
| `@svelte-lean/styles/slider.css` | slider (ADR 0011 §1.12) | – | 1258 B | 1047 B |
| `@svelte-lean/styles/autocomplete.css` | autocomplete | `control.css` | 1010 B | 867 B |
| `@svelte-lean/styles/accordion.css` | accordion (Tier 0, native \<details\> items, no wrapper) | – | 974 B | 817 B |
| `@svelte-lean/styles/card.css` | card (Tier 0, \<article\>) | – | 1218 B | 1007 B |
| `@svelte-lean/styles/separator.css` | separator (Tier 0, \<hr\> or role="separator") | – | 286 B | 219 B |
| `@svelte-lean/styles/avatar.css` | avatar (Tier 0, an \<img\> with initials behind it) | – | 578 B | 464 B |
| `@svelte-lean/styles/badge.css` | badge (Tier 0, \<span\> with an optional remove \<button\>) | – | 927 B | 763 B |
| `@svelte-lean/styles/kbd.css` | kbd (Tier 0, native \<kbd\>) | – | 372 B | 298 B |
| `@svelte-lean/styles/carousel.css` | carousel (Tier 0, scroll snapping and CSS carousels) | – | 986 B | 839 B |
| `@svelte-lean/styles/alert.css` | alert (Tier 0, a message block; role="status" or role="alert" only for a message the application writes after the page loaded) | – | 984 B | 814 B |
| `@svelte-lean/styles/progress.css` | progress (Tier 0, native \<progress\>) | – | 869 B | 723 B |
| `@svelte-lean/styles/meter.css` | meter (Tier 0, native \<meter\>) | – | 492 B | 392 B |
| `@svelte-lean/styles/skeleton.css` | skeleton (Tier 0, aria-hidden placeholders) | – | 566 B | 481 B |
| `@svelte-lean/styles/breadcrumb.css` | breadcrumb (Tier 0, \<nav\> + \<ol\> + aria-current="page") | – | 648 B | 532 B |
| `@svelte-lean/styles/nav.css` | nav (Tier 0, \<nav\> + \<ul\> of links + \<details name\> groups) | – | 1789 B | 1521 B |
| `@svelte-lean/styles/pagination.css` | pagination (Tier 0, \<nav\> + a list of links + aria-current="page") | – | 970 B | 824 B |
| `@svelte-lean/styles/steps.css` | steps (Tier 0, \<ol\> + aria-current="step") | – | 1102 B | 930 B |
| `@svelte-lean/styles/toggle.css` | toggle (Tier 1; the click behavior comes from @svelte-lean/primitives/toggle) | – | 914 B | 766 B |
| `@svelte-lean/styles/toggle-group.css` | toggle-group (Tier 1; keyboard and pressing come from @svelte-lean/primitives/toggle-group) | – | 880 B | 726 B |
| `@svelte-lean/styles/toolbar.css` | toolbar (Tier 1; roving focus comes from @svelte-lean/primitives/toolbar) | `toggle.css`,`toggle-group.css` | 482 B | 399 B |
| `@svelte-lean/styles/tree.css` | tree (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tree) | – | 860 B | 722 B |
| `@svelte-lean/styles/toast.css` | toast (Tier 2; a popover="manual" region with a live list) | – | 1254 B | 1073 B |
| `@svelte-lean/styles/button-group.css` | button-group (Tier 0, buttons in role="group") | `button.css` | 511 B | 405 B |
| `@svelte-lean/styles/scroll-area.css` | scroll-area (Tier 0, a focusable scrolling region) | – | 566 B | 453 B |
| `@svelte-lean/styles/descriptions.css` | descriptions (Tier 0, a \<dl\> of label and value pairs) | – | 555 B | 450 B |
| `@svelte-lean/styles/timeline.css` | timeline (Tier 0, \<ol\> of dated events) | – | 718 B | 599 B |
| `@svelte-lean/styles/empty.css` | empty (Tier 0, an empty state: content with a layout) | – | 690 B | 583 B |
| `@svelte-lean/styles/splitter.css` | splitter (Tier 2; the keyboard and the drag come from @svelte-lean/primitives/splitter) | – | 791 B | 658 B |
| `@svelte-lean/styles/sortable.css` | sortable (Tier 2; the drag, keys and buttons come from @svelte-lean/primitives/sortable) | – | 984 B | 801 B |
| `@svelte-lean/styles/file-drop.css` | file drop (Tier 1; the drag mark, the checks and the file list come from @svelte-lean/primitives/file-drop) | – | 1898 B | 1640 B |
| `@svelte-lean/styles/hover-card.css` | hover card (Tier 1 where interest invokers are missing; the card shows on hover or keyboard focus of a trigger) | `popover.css` | 386 B | 315 B |
| `@svelte-lean/styles/menubar.css` | menubar (Tier 1; the keys between menus come from @svelte-lean/primitives/menubar) | – | 598 B | 489 B |
| `@svelte-lean/styles/hint.css` | hint (Tier 1 attribute primitive; role="tooltip" + popover="manual") | – | 633 B | 517 B |
| `@svelte-lean/styles/copy.css` | copy (Tier 1 attribute primitive; navigator.clipboard.writeText) | – | 360 B | 279 B |
| `@svelte-lean/styles/findable.css` | findable (Tier 1 attribute primitive; hidden="until-found" + beforematch) | – | 423 B | 343 B |

Sizes from `packages/styles/artifacts/size.json` (`yarn size`: minified, gzip level 9, brotli quality 11).bytes = shipped source; minified = comments and redundant whitespace removed by scripts/check.mjs minify (a bundler minifier may be slightly smaller); gzip level 9 and brotli quality 11 of the minified text. standalone = the file with the files it @imports inlined. "index.css (resolved)" inlines every @import once, depth first, and carries the budget.

## Layers and specificity

- Every rule sits in a `slean.*` cascade layer and every selector in `:where()`: zero specificity, and import order does not matter.
- Stylesheets that restyle another primitive (menu on popover, button group, toolbar, context menu, date picker) use `slean.compositions`, after `slean.components`; the shared field shell (`control.css`) sits in `slean.components.shell`, below the component files.
- The reset touches only elements with `data-slean` or `data-slean-part`; content inside a dialog or a tab panel keeps the page's styles.

`button.css (excerpt)`

```css
/* Every file starts with the same layer statement, so import order between primitive files
 * does not matter. Every rule sits inside its layer and every selector is wrapped in :where(). */
@layer slean.reset, slean.tokens, slean.base, slean.components, slean.compositions, slean.utilities;

@layer slean.components {
	:where([data-slean='button']) {
		min-block-size: var(--slean-control-height-md);
		border-radius: var(--slean-radius-md);
		background: var(--slean-primary-main);
	}
}
```

An unlayered rule of yours wins, whatever its specificity: no `!important`.

`app.css`

```css
/* An unlayered rule in your application wins over the package, whatever its specificity. */
[data-slean='button'] {
	border-radius: 0;
}
```

## What is not provided

- No Tailwind dependency, CSS-in-JS or runtime style injection; it works next to [Tailwind CSS](https://svelte-lean.vercel.app/styling/tailwind.md).
- No JavaScript and no Provider: a theme is token values ([Themes](https://svelte-lean.vercel.app/styling/themes.md)).
- No positioning engine: CSS anchor positioning, with the platform's default placement where it is unsupported.
- No fonts: controls inherit the page font; `--slean-font-sans` is there to opt in.
- No `data-state` copies: styles select on `:checked`, `[open]`, `:popover-open` and ARIA ([Variants](https://svelte-lean.vercel.app/styling/variants.md)).

Source: packages/styles/css · scripts/check.mjs · README
