sveltelean
Versionv0.2.0 GitHub

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.

app.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

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.

+layout.svelte
// 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
// 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.

ImportContentsImportsgzipbrotli
@svelte-lean/styles/index.cssEverything below, each file once (convenience entry)(measured with every import resolved)–20765 B16880 B
@svelte-lean/styles/tokens.css--slean-* custom properties: light and dark palettes, aliases, forced colors, reduced motion, coarse pointer–2204 B1895 B
@svelte-lean/styles/base.cssLayer order, a reset scoped to [data-slean] elements, the shared focus ring, icon sizing–360 B290 B
@svelte-lean/styles/button.cssbutton (Tier 0)–1013 B863 B
@svelte-lean/styles/dialog.cssdialog (Tier 0, native <dialog>)–898 B728 B
@svelte-lean/styles/popover.csspopover (Tier 0, native [popover])–559 B465 B
@svelte-lean/styles/disclosure.cssdisclosure (Tier 0, native <details>)–638 B509 B
@svelte-lean/styles/checkbox.csscheckbox (Tier 0, native <input type="checkbox">)–764 B615 B
@svelte-lean/styles/switch.cssswitch (Tier 0, native <input type="checkbox" role="switch">)–771 B626 B
@svelte-lean/styles/radio-group.cssradio group (Tier 0, native <input type="radio"> sharing a name)–834 B688 B
@svelte-lean/styles/tabs.csstabs (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tabs)–835 B678 B
@svelte-lean/styles/menu.cssmenu (Tier 1; keyboard behavior comes from @svelte-lean/primitives/menu)popover.css993 B834 B
@svelte-lean/styles/listbox.csslistbox (Tier 1; keyboard behavior comes from @svelte-lean/primitives/listbox)–917 B752 B
@svelte-lean/styles/control.csscontrol shell (ADR 0007)–1960 B1698 B
@svelte-lean/styles/combobox.csscombobox (Tier 2; behavior comes from @svelte-lean/primitives/combobox)control.css178 B144 B
@svelte-lean/styles/date-field.cssdate field (Tier 0, native <input type="date|time|datetime-local|month| week">)–654 B550 B
@svelte-lean/styles/calendar.csscalendar (Tier 1, role="grid" of day buttons)–1312 B1121 B
@svelte-lean/styles/date-picker.cssdate picker (Tier 2; behavior comes from @svelte-lean/primitives/date-picker)control.css,calendar.css812 B687 B
@svelte-lean/styles/input.cssinput (Tier 0, native <input> of a text-like type and <textarea>)–680 B560 B
@svelte-lean/styles/field.cssfield (Tier 0: a layout for a label, a control, a description and an error)–455 B368 B
@svelte-lean/styles/select.cssselectcontrol.css1685 B1456 B
@svelte-lean/styles/segmented.csssegmented control (Tier 0, native <input type="radio"> sharing a name)–848 B691 B
@svelte-lean/styles/rating.cssrating (Tier 0, native <input type="radio"> sharing a name)–926 B746 B
@svelte-lean/styles/slider.cssslider (Tier 0, native <input type="range">)–777 B624 B
@svelte-lean/styles/otp-field.cssone-time code (Tier 0, one native <input autocomplete="one-time-code">)–818 B696 B
@svelte-lean/styles/file-field.cssfile field (Tier 0, native <input type="file">)–702 B598 B
@svelte-lean/styles/color-field.csscolor field (Tier 0, native <input type="color">)–602 B488 B
@svelte-lean/styles/autocomplete.cssautocomplete (Tier 0, native <input list> + <datalist>)–594 B482 B
@svelte-lean/styles/alert-dialog.cssalert dialog (Tier 0, native <dialog role="alertdialog" closedby="none">)–559 B457 B
@svelte-lean/styles/drawer.cssdrawer (Tier 0, native modal <dialog> at an edge of the viewport)–1152 B970 B
@svelte-lean/styles/accordion.cssaccordion (Tier 0, a group of native <details>)–816 B660 B
@svelte-lean/styles/card.csscard (Tier 0, <article>)–857 B686 B
@svelte-lean/styles/separator.cssseparator (Tier 0, <hr> or role="separator")–285 B219 B
@svelte-lean/styles/avatar.cssavatar (Tier 0, an <img> with initials behind it)–545 B437 B
@svelte-lean/styles/badge.cssbadge (Tier 0, <span>)–488 B387 B
@svelte-lean/styles/tag.csstag (Tier 0, <span> with an optional remove <button>)–731 B592 B
@svelte-lean/styles/kbd.csskbd (Tier 0, native <kbd>)–373 B297 B
@svelte-lean/styles/carousel.csscarousel (Tier 0, scroll snapping and CSS carousels)–978 B841 B
@svelte-lean/styles/alert.cssalert (Tier 0, a message block; role="status" or role="alert" only for a message the application writes after the page loaded)–798 B665 B
@svelte-lean/styles/progress.cssprogress (Tier 0, native <progress>)–601 B514 B
@svelte-lean/styles/meter.cssmeter (Tier 0, native <meter>)–486 B389 B
@svelte-lean/styles/spinner.cssspinner (Tier 0, role="status" with a text label)–510 B419 B
@svelte-lean/styles/skeleton.cssskeleton (Tier 0, aria-hidden placeholders)–546 B459 B
@svelte-lean/styles/breadcrumb.cssbreadcrumb (Tier 0, <nav> + <ol> + aria-current="page")–647 B532 B
@svelte-lean/styles/pagination.csspagination (Tier 0, <nav> + a list of links + aria-current="page")–891 B755 B
@svelte-lean/styles/steps.csssteps (Tier 0, <ol> + aria-current="step")–1047 B882 B
@svelte-lean/styles/toggle.csstoggle (Tier 1; the click behavior comes from @svelte-lean/primitives/toggle)–731 B603 B
@svelte-lean/styles/toggle-group.csstoggle-group (Tier 1; keyboard and pressing come from @svelte-lean/primitives/toggle-group)–787 B646 B
@svelte-lean/styles/toolbar.csstoolbar (Tier 1; roving focus comes from @svelte-lean/primitives/toolbar)toggle.css,toggle-group.css481 B399 B
@svelte-lean/styles/range-slider.cssrange slider (Tier 1; the ordering and the fill come from @svelte-lean/primitives/range-slider)–801 B689 B
@svelte-lean/styles/number-field.cssnumber field (Tier 1; the step buttons come from @svelte-lean/primitives/number-field)–987 B837 B
@svelte-lean/styles/tree.csstree (Tier 1; keyboard behavior comes from @svelte-lean/primitives/tree)–856 B719 B
@svelte-lean/styles/tooltip.csstooltip (Tier 1; popover="hint" + role="tooltip")–543 B443 B
@svelte-lean/styles/toast.csstoast (Tier 2; a popover="manual" region with a live list)–952 B794 B
@svelte-lean/styles/button-group.cssbutton-group (Tier 0, buttons in role="group")button.css503 B404 B
@svelte-lean/styles/scroll-area.cssscroll-area (Tier 0, a focusable scrolling region)–561 B457 B
@svelte-lean/styles/descriptions.cssdescriptions (Tier 0, a <dl> of label and value pairs)–554 B451 B
@svelte-lean/styles/timeline.csstimeline (Tier 0, <ol> of dated events)–750 B622 B
@svelte-lean/styles/empty.cssempty (Tier 0, an empty state: content with a layout)–663 B553 B
@svelte-lean/styles/splitter.csssplitter (Tier 2; the keyboard and the drag come from @svelte-lean/primitives/splitter)–779 B657 B
@svelte-lean/styles/context-menu.csscontext menu (Tier 1; opening at the pointer comes from @svelte-lean/primitives/context-menu)popover.css,menu.css246 B219 B
@svelte-lean/styles/file-drop.cssfile drop (Tier 1; the drag mark and the file list come from @svelte-lean/primitives/file-drop)–659 B548 B
@svelte-lean/styles/hover-card.csshover card (Tier 1; showing and hiding come from @svelte-lean/primitives/hover-card)–621 B516 B
@svelte-lean/styles/menubar.cssmenubar (Tier 1; the keys between menus come from @svelte-lean/primitives/menubar)–599 B498 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.

button.css (excerpt)
/* 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.

app.css
/* 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-sans is defined for the page to apply if it wants the token stack.
  • No mirrored state. Styles select on :checked, :disabled, [open], :popover-open and ARIA attributes, never on a data-state copy (Variants).

Source: packages/styles/css, the static checks in scripts/check.mjs and the package README.