@simple-base/css
v0.15.5
Published
Plain CSS component styles and typography for Simple Base.
Maintainers
Readme
@simple-base/css
Component styles and typography for Simple Base. Plain CSS, no build step, no runtime — the selectors are the API.
Styles cover components and their internal parts, including functional wrappers such as select arrows and scrollable tables. Page layouts, component galleries, and showcase helpers belong in your application.
Install
pnpm add @simple-base/cssQuick start
The package root includes the tokens and every component stylesheet. Import it once for its side effects:
import "@simple-base/css";Or from CSS:
@import "@simple-base/css";Neither form exports a JavaScript value or a CSS string — the import exists for its side effects.
To load only what you use, import the tokens first and then the components you need:
@import "@simple-base/tokens/css";
@import "@simple-base/css/button";
@import "@simple-base/css/badge";
@import "@simple-base/css/table";Then apply the root class and configuration attributes:
<button class="sb-button" data-variant="primary" data-size="medium">Save</button>
<span class="sb-badge" data-variant="success" data-size="small">Active</span>Theming
Colors come from @simple-base/tokens, which ships nine themes. Set data-theme on the root or on any nested region:
<html data-theme="nord"></html>Cascade layer
Every Simple Base rule lives in the sb cascade layer. The library declares that layer; your application decides where it sits by declaring the full order before the imports:
@layer theme, base, sb, components, utilities;
@import "tailwindcss";
@import "@simple-base/css";That order keeps resets out of the way and puts your own styles last:
| Layer | Holds | Relative to sb |
| ------------ | ------------------------ | ---------------- |
| theme | Tailwind theme variables | before |
| base | Tailwind preflight reset | before |
| sb | Every Simple Base rule | — |
| components | Application components | after |
| utilities | Tailwind utilities | after |
Layer priority is compared before specificity for normal declarations, so a later layer wins a conflict even when the earlier rule is more specific. Tailwind's preflight padding: 0 on * cannot strip a card's padding, while p-0 on that same card can set it.
Customize anything the library styles with one of these. None of them needs !important:
<!-- Tailwind utility -->
<div class="sb-card p-0">…</div>/* Application component, in the components layer */
@layer components {
.card-flush {
padding: 0;
}
}/* Unlayered application CSS — always wins */
.checkout .sb-card {
padding: var(--sb-semantic-space-4);
}<!-- Inline style -->
<div class="sb-card" style="padding: 1rem">…</div>The library sits before all of your layers, so it never needs !important. Reserve that for conflicts between your own styles, such as one utility that has to beat another.
If another layered framework shares the page, name its layers in the same statement and place them deliberately instead of relying on import order.
Migrating to 0.2.0
Card padding is no longer an option — it is part of the .sb-card surface:
data-paddingwas removed from this package.CardOptions.paddingandcardDefaultswere removed from @simple-base/contracts.- Base and flat cards that were previously unpadded now receive the stylesheet's default interior padding.
- Rule cards keep top-only padding.
variantis unchanged:flat,rule, or omitted for the base surface.
If a layout needs the previous unpadded look, override it from your own styles:
<!-- Tailwind utility, given the layer order above -->
<div class="sb-card p-0">…</div>/* Application component */
@layer components {
.card-flush {
padding: 0;
}
}
/* Unlayered application CSS */
.checkout-summary .sb-card {
padding: 0;
}Individual stylesheets
Each entry point is an explicit, extensionless subpath. The internal styles/ directory and .css filenames are not public import paths.
alert-dialog · badge · breadcrumb · button · card · checkbox · combobox · date-picker · dialog · disclosure · empty-state · field · input · keyboard-shortcut · menu · number-field · pagination · progress · radio · range · segmented-control · select · status · switch · table · tabs · textarea · tooltip · typography
Design conventions
The default Simple Base themes follow the Studio direction: warm paper neutrals, a forest green accent, and type chosen for legibility.
- Body text uses Atkinson Hyperlegible Next and code uses Atkinson Hyperlegible Mono.
.sb-displayand.sb-heading-1through.sb-heading-3use the Young Serif display face at a regular weight;.sb-heading-4through.sb-heading-6stay in the body face. Headings are sized 36, 28, 24, 20, 18, and 16px. The package names these families but does not download them — load the fonts in your app, or the system fallbacks apply. - Write captions, table headings, and optional dialog kickers in sentence case. Monospace is for code, identifiers, shortcuts, and alignment-dependent values.
- Status never relies on color alone. Success, warning, danger, and info badges include a glyph, field errors lead with a warning glyph, and the switch thumb shows a check or a cross. Glyphs, status dots, and alert marks take the status
basecolor, so the status stays visible in themes whose palette color is too light or dark to set the label in. - The accent badge is a solid accent fill, like the primary button, so it stays readable in every ported palette.
- Depth comes from edges, not blur. Cards use a border and a 1px baseline; blurred shadows are reserved for menus, popovers, dialogs, and toasts. Modal backdrops separate context with a scrim, not blur.
- Every
.sb-cardships its own interior padding; override.sb-cardfrom application CSS when a layout needs different spacing. - Buttons stay in place on hover; pressing scales them slightly, except under
prefers-reduced-motion. Disabled buttons drop their color rather than fading, so the label stays readable. - Focus is a solid 2px ring in the theme's focus color. Standalone controls offset it by 2px; bordered fields draw it flush with their border.
Selector conventions
.sb-<component>is the required root class..sb-<component>-<part>is a part of that component.- Configuration uses
data-*attributes; state usesdata-*,aria-*, or native pseudo-classes (:checked,:disabled,:hover,:focus-visible,[open]). - Element selectors only appear as descendants of a root class, to style the structure a component renders. They are not standalone hooks.
Variants and options
| Attribute | Component | Values |
| -------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| data-variant | .sb-badge | default, success, danger, warning, info, accent, command, outline, muted |
| data-variant | .sb-button | primary, secondary, tertiary, ghost, danger, danger-subtle |
| data-variant | .sb-card | flat, rule |
| data-variant | .sb-table cell | code, number |
| data-size | .sb-badge | small, medium |
| data-size | .sb-button | small, medium, large |
| data-variant | .sb-menu-item | danger |
| data-status | .sb-status-line, .sb-alert | success, warning, danger, info |
| data-status | .sb-toast | success, warning |
| data-status | .sb-progress | warning, danger, info; omit for success |
State attributes
| Selector | Applies to |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [aria-invalid="true"] | .sb-input, .sb-textarea, .sb-checkbox, .sb-radio |
| [data-invalid] | .sb-select-control, .sb-combobox-control, .sb-number-field-control, .sb-date-picker-control, .sb-field-description |
| [aria-selected="true"] | .sb-tab, .sb-table tbody tr |
| [aria-pressed="true"], [aria-checked="true"], [data-state="checked"] | .sb-segment |
| [aria-sort="ascending"], [aria-sort="descending"] | .sb-table th (styles its .sb-table-sort-button) |
| [aria-current="page"] | .sb-page-button, .sb-breadcrumb descendants |
| [aria-disabled="true"], :disabled | .sb-button, .sb-select, .sb-tab, .sb-segment, .sb-page-button, form controls |
| [data-disabled] | .sb-tab, .sb-segment, .sb-page-button, .sb-menu-item, .sb-number-field-control, .sb-number-field-trigger, .sb-date-picker-control, .sb-date-picker-nav-trigger, .sb-date-picker-cell-trigger; on .sb-field-label, .sb-field-title, .sb-field-description, or any ancestor of them |
| [data-state="open"] | .sb-select-trigger, .sb-combobox-trigger, .sb-date-picker-trigger, .sb-tooltip-content |
| [data-highlighted] | .sb-select-item, .sb-combobox-item, .sb-menu-item |
| [data-placeholder-shown], [data-required] | .sb-select, .sb-combobox |
| [data-required] | .sb-field-label, .sb-field-title |
| [data-state="indeterminate"] | .sb-progress |
| [data-instant] | .sb-tooltip-content (skips the enter animation) |
| [data-readonly] | .sb-date-picker (removes the trigger's hover state) |
| [data-inline] | .sb-date-picker-content |
| [data-view], [data-selected], [data-today], [data-outside-range], [data-in-range], [data-range-start], [data-range-end], [data-unavailable] | .sb-date-picker-cell-trigger |
| [open] | .sb-dialog-content, .sb-alert-dialog-content, .sb-disclosure, .sb-menu |
| [data-state="open"], [data-state="closed"] | .sb-toaster .sb-toast |
Component reference
| Component | Root | Parts |
| ----------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Alert dialog | .sb-alert-dialog-content | -trigger, -icon, -header, -kicker, -title, -description, -footer, -cancel, -action |
| Badge | .sb-badge | — |
| Breadcrumb | .sb-breadcrumb | -list, -item, -link, -separator |
| Button | .sb-button | — |
| Card | .sb-card | — |
| Checkbox | .sb-checkbox | — |
| Combobox | .sb-combobox | -control, -input, -trigger, -content, -list, -item, -empty |
| Date picker | .sb-date-picker | -control, -input, -trigger, -content, -calendar, -view-control, -view-trigger, -nav-trigger, -table, -table-header, -table-cell, -cell-trigger, -presets |
| Dialog | .sb-dialog-content | -trigger, -header, -title, -kicker, -description, -footer, -close, -action |
| Disclosure | .sb-disclosure | -copy |
| Empty state | .sb-empty-state | -mark (alias .sb-empty-mark), -title, -description, -actions |
| Field | .sb-field, .sb-fieldset | -label, -title, -description, -error, .sb-choice-list, .sb-choice, .sb-visually-hidden |
| Input | .sb-input | — |
| Keyboard shortcut | .sb-shortcut | — |
| Menu (popup) | .sb-menu-content | .sb-menu-item, -item-shortcut, -group-label, -separator |
| Menu (native) | .sb-menu (on details) | -panel, -item, -item-shortcut, -group-label, -separator |
| Number field | .sb-number-field | -control, -input, -affix, -trigger |
| Pagination | .sb-pagination | .sb-page-button, .sb-page-ellipsis |
| Progress | .sb-progress | -value |
| Radio | .sb-radio | — |
| Range | .sb-range | .sb-range-readout |
| Segmented control | .sb-segmented-control (alias .sb-segmented) | .sb-segment |
| Select | .sb-select-root (compound) | -control, -trigger, -value-text, -indicator, -content, -list, -item, -empty |
| Select (native) | .sb-select-wrap | .sb-select |
| Status | .sb-status-line, .sb-alert, .sb-toast | -dot, -icon, .sb-status-line-content, .sb-status-line-title, .sb-alert-mark, .sb-alert-content, .sb-alert-title, .sb-alert-actions, .sb-alert-close, .sb-live-region |
| Toast | .sb-toaster (region), .sb-toast | -icon, -content, -title, -description, -action, -close |
| Switch | .sb-switch | — |
| Table | .sb-table-wrap, .sb-table | .sb-table-sort-button, .sb-table-sort-indicator |
| Tabs | .sb-tabs | .sb-tab, .sb-tab-panel |
| Textarea | .sb-textarea | — |
| Tooltip | .sb-tooltip-content | -arrow, -arrow-tip |
| Typography | .sb-display, .sb-heading-1 … .sb-heading-6, .sb-text-*, .sb-code-* | — |
.sb-checkbox:indeterminate draws a dash for a partly selected group. indeterminate is a DOM property with no HTML attribute, so set it from script (input.indeterminate = true); the browser clears it when the user toggles the box.
For a sortable column, put a button.sb-table-sort-button as the only child of the th, ending with an empty span.sb-table-sort-indicator marked aria-hidden. The indicator shows both directions while unsorted and an arrow when the th has aria-sort="ascending" or "descending". Set aria-sort on the sorted column only; sorting stays in your app.
Form text
Every form component uses the same label, description, and error classes, so their text looks identical wherever it appears. Select, Combobox, Number field, and Date picker have no label, description, or error classes of their own:
<div class="sb-number-field">
<label class="sb-field-label" for="quantity" data-required>Quantity</label>
<div class="sb-number-field-control">…</div>
<p class="sb-field-description" id="quantity-description">Whole units only.</p>
<p class="sb-field-error" id="quantity-error">Enter at least 1.</p>
</div>These classes live in the field stylesheet. The select, combobox, number-field, and date-picker stylesheets import it themselves, so loading any of them alone still styles its text.
Element selectors
These rules style the markup a component renders. Use the listed elements, or add a part class where one is available.
| Parent | Elements |
| ----------------------------------------------------------------------------------------- | ---------------------------- |
| .sb-alert-dialog-icon, .sb-alert-mark, .sb-select-indicator, .sb-combobox-trigger | svg |
| .sb-menu-item, .sb-tab, .sb-page-button, .sb-empty-state-mark, .sb-alert-close | svg |
| .sb-number-field-trigger, .sb-date-picker-trigger, .sb-date-picker-nav-trigger | svg |
| .sb-segment | input (visually hidden) |
| .sb-tooltip-content | .sb-shortcut |
| .sb-breadcrumb | a |
| .sb-disclosure, .sb-menu | summary |
| .sb-empty-state | p |
| .sb-status-line, .sb-alert, .sb-toast | strong, p |
| .sb-field | label, legend, small |
| .sb-fieldset | legend |
| .sb-choice | span, input (via :has) |
| .sb-progress | span |
| .sb-table | th, td, tbody, tr |
Positioned popups
Select, combobox, date picker, menu, and tooltip content are positioned the same way: the Zag.js popper writes --x, --y, --z-index, --available-width, --available-height, and --transform-origin on the positioner, and the content reads them. The tooltip arrow reads --arrow-size and --arrow-background from .sb-tooltip-content. @simple-base/solid provides that behavior for select, combobox, date picker, menu, and tooltip. Without the Solid package, position the content yourself.
The native menu (.sb-menu on <details>) keeps its absolute .sb-menu-panel for CSS-only use. It is a disclosure of buttons, not an ARIA menu.
Toast region
Toast styles ship with status. Inside .sb-toaster, toasts are positioned by the --x, --y, --scale, --height, --opacity, and --z-index variables that the Zag.js toast machine writes; @simple-base/solid provides that behavior. Outside a region, .sb-toast is a static surface for CSS-only use inside .sb-live-region.
Versioning
Simple Base is on 0.x. All its packages go to 1.0.0 together, once @simple-base/react ships the same components as @simple-base/solid. Until then a minor release can include breaking changes.
