Styling
Svelte Lean primitives are plain HTML with a data-slean protocol; how they look is a separate decision. @svelte-lean/styles is the optional visual layer: design tokens and one stylesheet per primitive, CSS only, with no JavaScript, no build step and no dependency.
On this page
Three modes
The behavior contract is the same in every mode. A tab list works the same way whether it is
unstyled, styled by the package or styled by a file you own, because the behavior reads and
writes attributes (aria-selected, hidden) and never a class name.
Mode 1: your own CSS
Write CSS against the protocol attributes and the platform's own state selectors. Nothing from this package is required, and the markup carries no class name that a stylesheet has to know.
/* 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
Import the tokens, the base file and only the primitive files you use. This is the documented default and the path this site takes for its own controls.
// 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';// Everything, for a prototype or when most primitives are in use. Not the primary path.
import '@svelte-lean/styles';Mode 3: owned styles
Copy a primitive's stylesheet into the application and edit it there; the behavior stays in the package. The steps, and the status of the planned command, are on Owned styles.
Entry points
Every file is a subpath export of the package, listed here from its package.json.
Sizes are read from packages/styles/artifacts/size.json, written by the package's yarn size (gzip level 9, brotli quality 11 of the minified text). tokens.css and base.css are required by every primitive file. A file that builds on another imports
it itself, so every file works when it is the only one imported: the Imports column lists those files,
read from the stylesheets. A bundler includes a file imported twice once; without one, the second
copy changes nothing, because the imported rules sit in an earlier layer (or, for the control shell,
a sublayer) than the rules of the file that imports them.
| Import | Contents | Imports | gzip | brotli |
|---|---|---|---|---|
@svelte-lean/styles/index.css | Everything below, each file once (convenience entry)(measured with every import resolved) | – | 20765 B | 16880 B |
@svelte-lean/styles/tokens.css | --slean-* custom properties: light and dark palettes, aliases, forced colors, reduced motion, coarse pointer | – | 2204 B | 1895 B |
@svelte-lean/styles/base.css | Layer order, a reset scoped to [data-slean] elements, the shared focus ring, icon sizing | – | 360 B | 290 B |
@svelte-lean/styles/button.css | button (Tier 0) | – | 1013 B | 863 B |
@svelte-lean/styles/dialog.css | dialog (Tier 0, native <dialog>) | – | 898 B | 728 B |
@svelte-lean/styles/popover.css | popover (Tier 0, native [popover]) | – | 559 B | 465 B |
@svelte-lean/styles/disclosure.css | disclosure (Tier 0, native <details>) | – | 638 B | 509 B |
@svelte-lean/styles/checkbox.css | checkbox (Tier 0, native <input type="checkbox">) | – | 764 B | 615 B |
@svelte-lean/styles/switch.css | switch (Tier 0, native <input type="checkbox" role="switch">) | – | 771 B | 626 B |
@svelte-lean/styles/radio-group.css | radio group (Tier 0, native <input type="radio"> sharing a name) | – | 834 B | 688 B |
@svelte-lean/styles/tabs.css | tabs (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tabs) | – | 835 B | 678 B |
@svelte-lean/styles/menu.css | menu (Tier 1; keyboard behavior comes from @svelte-lean/primitives/menu) | popover.css | 993 B | 834 B |
@svelte-lean/styles/listbox.css | listbox (Tier 1; keyboard behavior comes from @svelte-lean/primitives/listbox) | – | 917 B | 752 B |
@svelte-lean/styles/control.css | control shell (ADR 0007) | – | 1960 B | 1698 B |
@svelte-lean/styles/combobox.css | combobox (Tier 2; behavior comes from @svelte-lean/primitives/combobox) | control.css | 178 B | 144 B |
@svelte-lean/styles/date-field.css | date field (Tier 0, native <input type="date|time|datetime-local|month| week">) | – | 654 B | 550 B |
@svelte-lean/styles/calendar.css | calendar (Tier 1, role="grid" of day buttons) | – | 1312 B | 1121 B |
@svelte-lean/styles/date-picker.css | date picker (Tier 2; behavior comes from @svelte-lean/primitives/date-picker) | control.css,calendar.css | 812 B | 687 B |
@svelte-lean/styles/input.css | input (Tier 0, native <input> of a text-like type and <textarea>) | – | 680 B | 560 B |
@svelte-lean/styles/field.css | field (Tier 0: a layout for a label, a control, a description and an error) | – | 455 B | 368 B |
@svelte-lean/styles/select.css | select | control.css | 1685 B | 1456 B |
@svelte-lean/styles/segmented.css | segmented control (Tier 0, native <input type="radio"> sharing a name) | – | 848 B | 691 B |
@svelte-lean/styles/rating.css | rating (Tier 0, native <input type="radio"> sharing a name) | – | 926 B | 746 B |
@svelte-lean/styles/slider.css | slider (Tier 0, native <input type="range">) | – | 777 B | 624 B |
@svelte-lean/styles/otp-field.css | one-time code (Tier 0, one native <input autocomplete="one-time-code">) | – | 818 B | 696 B |
@svelte-lean/styles/file-field.css | file field (Tier 0, native <input type="file">) | – | 702 B | 598 B |
@svelte-lean/styles/color-field.css | color field (Tier 0, native <input type="color">) | – | 602 B | 488 B |
@svelte-lean/styles/autocomplete.css | autocomplete (Tier 0, native <input list> + <datalist>) | – | 594 B | 482 B |
@svelte-lean/styles/alert-dialog.css | alert dialog (Tier 0, native <dialog role="alertdialog" closedby="none">) | – | 559 B | 457 B |
@svelte-lean/styles/drawer.css | drawer (Tier 0, native modal <dialog> at an edge of the viewport) | – | 1152 B | 970 B |
@svelte-lean/styles/accordion.css | accordion (Tier 0, a group of native <details>) | – | 816 B | 660 B |
@svelte-lean/styles/card.css | card (Tier 0, <article>) | – | 857 B | 686 B |
@svelte-lean/styles/separator.css | separator (Tier 0, <hr> or role="separator") | – | 285 B | 219 B |
@svelte-lean/styles/avatar.css | avatar (Tier 0, an <img> with initials behind it) | – | 545 B | 437 B |
@svelte-lean/styles/badge.css | badge (Tier 0, <span>) | – | 488 B | 387 B |
@svelte-lean/styles/tag.css | tag (Tier 0, <span> with an optional remove <button>) | – | 731 B | 592 B |
@svelte-lean/styles/kbd.css | kbd (Tier 0, native <kbd>) | – | 373 B | 297 B |
@svelte-lean/styles/carousel.css | carousel (Tier 0, scroll snapping and CSS carousels) | – | 978 B | 841 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) | – | 798 B | 665 B |
@svelte-lean/styles/progress.css | progress (Tier 0, native <progress>) | – | 601 B | 514 B |
@svelte-lean/styles/meter.css | meter (Tier 0, native <meter>) | – | 486 B | 389 B |
@svelte-lean/styles/spinner.css | spinner (Tier 0, role="status" with a text label) | – | 510 B | 419 B |
@svelte-lean/styles/skeleton.css | skeleton (Tier 0, aria-hidden placeholders) | – | 546 B | 459 B |
@svelte-lean/styles/breadcrumb.css | breadcrumb (Tier 0, <nav> + <ol> + aria-current="page") | – | 647 B | 532 B |
@svelte-lean/styles/pagination.css | pagination (Tier 0, <nav> + a list of links + aria-current="page") | – | 891 B | 755 B |
@svelte-lean/styles/steps.css | steps (Tier 0, <ol> + aria-current="step") | – | 1047 B | 882 B |
@svelte-lean/styles/toggle.css | toggle (Tier 1; the click behavior comes from @svelte-lean/primitives/toggle) | – | 731 B | 603 B |
@svelte-lean/styles/toggle-group.css | toggle-group (Tier 1; keyboard and pressing come from @svelte-lean/primitives/toggle-group) | – | 787 B | 646 B |
@svelte-lean/styles/toolbar.css | toolbar (Tier 1; roving focus comes from @svelte-lean/primitives/toolbar) | toggle.css,toggle-group.css | 481 B | 399 B |
@svelte-lean/styles/range-slider.css | range slider (Tier 1; the ordering and the fill come from @svelte-lean/primitives/range-slider) | – | 801 B | 689 B |
@svelte-lean/styles/number-field.css | number field (Tier 1; the step buttons come from @svelte-lean/primitives/number-field) | – | 987 B | 837 B |
@svelte-lean/styles/tree.css | tree (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tree) | – | 856 B | 719 B |
@svelte-lean/styles/tooltip.css | tooltip (Tier 1; popover="hint" + role="tooltip") | – | 543 B | 443 B |
@svelte-lean/styles/toast.css | toast (Tier 2; a popover="manual" region with a live list) | – | 952 B | 794 B |
@svelte-lean/styles/button-group.css | button-group (Tier 0, buttons in role="group") | button.css | 503 B | 404 B |
@svelte-lean/styles/scroll-area.css | scroll-area (Tier 0, a focusable scrolling region) | – | 561 B | 457 B |
@svelte-lean/styles/descriptions.css | descriptions (Tier 0, a <dl> of label and value pairs) | – | 554 B | 451 B |
@svelte-lean/styles/timeline.css | timeline (Tier 0, <ol> of dated events) | – | 750 B | 622 B |
@svelte-lean/styles/empty.css | empty (Tier 0, an empty state: content with a layout) | – | 663 B | 553 B |
@svelte-lean/styles/splitter.css | splitter (Tier 2; the keyboard and the drag come from @svelte-lean/primitives/splitter) | – | 779 B | 657 B |
@svelte-lean/styles/context-menu.css | context menu (Tier 1; opening at the pointer comes from @svelte-lean/primitives/context-menu) | popover.css,menu.css | 246 B | 219 B |
@svelte-lean/styles/file-drop.css | file drop (Tier 1; the drag mark and the file list come from @svelte-lean/primitives/file-drop) | – | 659 B | 548 B |
@svelte-lean/styles/hover-card.css | hover card (Tier 1; showing and hiding come from @svelte-lean/primitives/hover-card) | – | 621 B | 516 B |
@svelte-lean/styles/menubar.css | menubar (Tier 1; the keys between menus come from @svelte-lean/primitives/menubar) | – | 599 B | 498 B |
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
All rules live in six cascade layers, and every selector is wrapped in :where(),
which contributes no specificity. The order is declared at the top of every file, so the order
in which you import primitive files does not matter. A stylesheet that restyles another
primitive (a menu is a popover, a button group joins buttons, a toolbar flattens toggles, a
context menu places a menu, a date picker lays out a calendar) keeps its rules in slean.compositions, after slean.components: it wins even where a
bundler loads the restyled file later. The field shell every owned form control shares (control.css) sits in the sublayer slean.components.shell, below every rule of the component
files.
/* 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-accent);
}
}Because layered rules lose to unlayered ones regardless of specificity, an override is a plain
rule in your own stylesheet. No !important, no selector escalation.
/* An unlayered rule in your application wins over the package, whatever its specificity. */
[data-slean='button'] {
border-radius: 0;
}The reset touches only elements that carry data-slean or data-slean-part. Content nested inside a primitive, such as the body of a dialog or
a tab panel, keeps the page's own styles: a global h2 rule applies to a <h2 data-slean-part="title"> as it applies anywhere else.
What is not provided
- No Tailwind dependency, no CSS-in-JS, no runtime style injection. The package works next to Tailwind CSS: Tailwind CSS gives the layer order and the token mappings.
- No JavaScript. The package is the files in
css/; a change of theme is a change of token values, applied by the cascade. - No Provider. Theme values belong in CSS, behavior routing attaches to the document, so no component has to wrap the application (Themes).
- No positioning engine. Popovers and menus are placed with CSS anchor positioning and keep the platform's default placement where it is unsupported.
- No fonts. Controls inherit the page font;
--slean-font-sansis defined for the page to apply if it wants the token stack. - No mirrored state. Styles select on
:checked,:disabled,[open],:popover-openand ARIA attributes, never on adata-statecopy (Variants).
Source: packages/styles/css, the static checks in scripts/check.mjs and the package README.