@oleksandr-94/aura-css
v0.1.0
Published
Aura — a framework-agnostic, themeable, skinnable CSS component library (SCSS source + optional JS behaviour layer).
Maintainers
Readme
Aura
A framework-agnostic, themeable, skinnable CSS component library authored in SCSS. Works in any project — plain HTML, React/Vue/Svelte, or alongside Tailwind — via a single stylesheet. Theming and surface style ("skin") are decoupled from markup, so the same components can be glass in one app and flat in another, on different palettes.
Status: early foundation. Complete theming/skin/token system + 2 components (
button,card). More components are being added on top of this base.
Quick start
# from aura/
npm install # sass + postcss + esbuild toolchain (dev only)
npm run build # → dist/ : CSS (expanded + min, per skin) + JS bundles + typesInclude the stylesheet and set a theme on the root element:
<html data-theme="aura-dark">
<head>
<link rel="stylesheet" href="dist/aura.css" />
</head>
<body>
<button class="btn">Primary</button>
<div class="card"><h3 class="card__title">Hello</h3></div>
</body>
</html>No build in your consuming project? Just ship the compiled dist/aura.css.
The three config levers
Everything adapts through src/_config.scss. Override with Sass configuration:
@use "aura/src/index" with (
$prefix: 'au-', // namespace all classes → .au-btn (default: none)
$skin: flat, // surface treatment: glass | flat | neu
$themes: ( ... ) // your colour palettes (see Theming)
);| Lever | What it controls | Default |
|-------|------------------|---------|
| $prefix | Class-name namespace (avoid collisions with other libs) | '' |
| $skin | Surface look — glass / flat / neu | glass |
| $themes | Colour palettes, swapped at runtime via data-theme | dark + light |
Theming
Colours follow Material 3 color roles:
every fill colour has a paired on-* content colour for guaranteed contrast, and a
subtle *-container tint for badges/alerts.
You only define base colours — each on-* (text-on-colour) is derived
automatically by luminance, so "white-on-white" is structurally impossible.
Token contract
Components reference only these CSS variables (never hardcoded colours):
Accents & status — each has --<role>, --on-<role>, --<role>-container, --on-<role>-container:
primary, secondary, accent, success, warning, error, info
Surfaces & lines
| Token | Use |
|-------|-----|
| --surface | Page background |
| --surface-1 / --surface-2 | Card / elevated (modal, popover) |
| --on-surface / --on-surface-muted | Body text / secondary text |
| --outline / --outline-strong | Dividers / control borders |
| --glass-film / --glass-film-2 | Frosted film (glass skin only) |
| --shadow-1 / --shadow-2 | Elevation |
Static scales (theme-independent)
--radius-sm|md|lg|pill · --space-1|2|3|4|6|8 · --font · --fs-xs|sm|md|lg|xl ·
--blur · --z-dropdown|sticky|modal|toast ·
--gradient (general brand gradient, auto-built from the theme's accents)
Add a theme
Quickest: open docs/theme-generator.html — pick accent colours + a base mode, get a
live preview and a ready-to-paste [data-theme] block (with on-* and containers derived).
Or add an entry to $themes — only base colours required:
$themes: (
ocean: (
primary: #0EA5E9, secondary: #6366F1, accent: #22D3EE,
success: #16A34A, warning: #D97706, error: #DC2626, info: #3B82F6,
surface: #0b1220, surface-1: #131c2e, surface-2: #1b2740,
on-surface: #e6edf7, on-surface-muted: rgba(230,237,247,.62),
outline: rgba(255,255,255,.14), outline-strong: rgba(255,255,255,.24),
glass-film: rgba(255,255,255,.06), glass-film-2: rgba(255,255,255,.10),
shadow-1: (0 8px 32px rgba(0,0,0,.4)), shadow-2: (0 16px 44px rgba(0,0,0,.5)),
),
);Rebuild, then use it: <html data-theme="ocean">. Themes can also be layered per
subtree — nest data-theme on any element.
Skins
The skin is the surface treatment, independent of colour. Components request their
chrome via three mixins — surface() (panels/cards), field() (input wells) and
control() (filled buttons) — and the active skin renders each. So buttons, inputs and
cards all adapt together.
| Skin | Card (surface) | Button (control) | Input (field) |
|------|------------------|--------------------|-----------------|
| glass | Frosted film + blur | Solid + soft glow + gloss | Translucent well + blur |
| flat | Solid + border | Solid fill | Solid + border |
| neu | Extruded shadows | Extruded, inset on press | Inset well |
Switch at build time with $skin. Each skin lives in its own file under src/skins/;
adding a new one = new file + one branch in src/_surface.scss.
Prebuilt skin outputs: dist/aura.css (glass, default), dist/aura-flat.css,
dist/aura-neu.css.
Components
Button — .btn
Base class renders the primary button. Add modifiers to change colour, style, size.
| Class | Effect |
|-------|--------|
| .btn | Primary (default) |
| .btn--secondary / .btn--accent | Accent colours |
| .btn--success / .btn--warning / .btn--error / .btn--info | Status colours |
| .btn--ghost | Transparent, bordered |
| .btn--outline | Outlined; combine with a colour modifier |
| .btn--gradient | Gradient fill — follows the colour; combine with a colour modifier (.btn--gradient.btn--success) |
| .btn--sm / .btn--lg | Sizes |
| .btn--block | Full width |
| .btn--disabled / [disabled] | Disabled state |
States handled automatically: :hover, :active, :focus-visible, :disabled.
<button class="btn">Primary</button>
<button class="btn btn--secondary">Secondary</button>
<button class="btn btn--error btn--outline">Delete</button>
<button class="btn btn--lg btn--block">Continue</button>
<button class="btn" disabled>Disabled</button>Card — .card
Surface container rendered through the active skin (glass/flat/neu).
| Class | Element / effect |
|-------|------------------|
| .card | Container (padded, uses surface()) |
| .card__header / .card__title / .card__body / .card__actions | Header row / heading / text / footer |
| .card--success / --warning / --error / --info | Coloured status accent (left bar) |
| .card--gradient | Gradient fill; combine with a colour modifier |
<div class="card card--success">
<div class="card__header">
<h3 class="card__title">Acme Inc.</h3>
<span class="badge badge--success">Active</span>
</div>
<p class="card__body">Enterprise plan · renews 12 Aug.</p>
</div>
<div class="card card--gradient card--error"> … </div>Input — .input / .select / .textarea
Text controls whose "well" is rendered through the field() mixin, so they adopt the
active skin (glass / flat / neu). Wrap with .field to attach a label and hint.
| Class | Effect |
|-------|--------|
| .input / .select / .textarea | Base controls |
| .input--sm / .input--lg | Sizes |
| .input--error (also --select/--textarea) | Error state (red border) |
| .field | Labelled wrapper (label + control + hint) |
| .field__label | Label |
| .field__hint / .field__hint--error | Helper / error text |
States handled automatically: :focus-visible (primary ring), :disabled.
<div class="field">
<label class="field__label" for="email">Email</label>
<input id="email" class="input" type="email" placeholder="[email protected]">
</div>
<div class="field">
<label class="field__label" for="pw">Password</label>
<input id="pw" class="input input--error" type="password">
<span class="field__hint field__hint--error">At least 8 characters.</span>
</div>Badge — .badge
Small status label. Colour variants use soft *-container tints with a guaranteed-contrast
content colour.
| Class | Effect |
|-------|--------|
| .badge | Base (neutral) |
| .badge--primary / --secondary / --accent | Accent tints |
| .badge--success / --warning / --error / --info | Status tints |
| .badge--outline | Transparent, bordered |
| .badge--sm / .badge--lg | Sizes |
| .badge__dot | Leading status dot |
<span class="badge badge--success"><i class="badge__dot"></i> Active</span>
<span class="badge badge--error badge--outline">Failed</span>Alert — .alert
A message panel with an accent bar and icon. Colour variants carry status meaning.
| Class | Effect |
|-------|--------|
| .alert | Base (primary) panel |
| .alert--success / --warning / --error / --info | Status colours |
| .alert__icon | Leading icon |
| .alert__title / .alert__body | Bold heading / muted description |
<div class="alert alert--error">
<span class="alert__icon">✕</span>
<div>
<div class="alert__title">Payment failed</div>
<div class="alert__body">Your card was declined.</div>
</div>
</div>Switch — .switch
A toggle for boolean settings. The track is a skin-aware well; when checked it fills with the accent colour and shows a white thumb.
| Class | Effect |
|-------|--------|
| .switch | Toggle (wraps a checkbox) |
| .switch__track / .switch__thumb | Track / knob |
| .switch--success / --warning / --error | Checked colour |
| .switch--sm / .switch--lg | Sizes |
<label class="switch">
<input type="checkbox" checked>
<span class="switch__track"></span>
<span class="switch__thumb"></span>
</label>Checkbox — .checkbox · Radio — .radio
Custom controls set directly on the native input. The box/circle is a skin-aware well;
checked fills with the accent colour + white mark. :indeterminate (checkbox) shows a dash.
| Class | Effect |
|-------|--------|
| .checkbox / .radio | On the native input |
| .checkbox--success / --warning / --error | Checked colour |
| .radio--success / --warning / --error | Checked colour |
<input type="checkbox" class="checkbox" checked>
<input type="radio" name="plan" class="radio radio--success" checked>Select — .select
Native <select> with a custom chevron. Same sizes (--sm/--lg) and validation
(--error/--success) as text inputs.
Range — .range · File — .file · Rating — .rating
| Class | Effect |
|-------|--------|
| .range | Themed slider · --success/--warning/--error · --sm/--lg |
| .file | Styled file input · .file--ghost |
| .rating | Star rating on radios (CSS-only, submittable) · --sm/--lg |
<input class="range range--success" type="range" value="80">
<input class="file" type="file">
<div class="rating">
<input type="radio" name="r" value="5"><label>★</label>
<input type="radio" name="r" value="4" checked><label>★</label>
…
</div>Validation — add --error / --success to any control; use .field__label--required
(adds *) and .field__hint--error / --success for messages.
Tabs — .tabs
A tab bar with switchable panels. Point each .tab at a panel with data-tab="#id"; the
JS activates it and toggles panel visibility. Frameworks render active tab + panel from state.
| Class | Effect |
|-------|--------|
| .tabs | Container: pill (default) · --underline / --lift · --sm / --lg · --block |
| .tab / .tab--active | Tab item / selected |
| .tab-panel | Content panel (toggled via hidden) |
| .tab-panels | Panel container (plain by default; card after --lift tabs) |
| .tab-card | Wraps .tabs + .tab-panels into one connected card (no gap) |
<div class="tabs" role="tablist">
<button class="tab tab--active" data-tab="#p1">Overview</button>
<button class="tab" data-tab="#p2">Activity</button>
</div>
<div id="p1" class="tab-panel">Overview…</div>
<div id="p2" class="tab-panel" hidden>Activity…</div>Group — .group (alias .btn-group)
Joins adjacent controls — buttons and form fields — into one segmented unit (square
inner corners, single shared border). Fields grow, buttons stay natural. .group--block
for full width.
<!-- button group -->
<div class="group">
<button class="btn btn--ghost">Day</button>
<button class="btn btn--ghost">Week</button>
</div>
<!-- input group -->
<div class="group">
<input class="input" placeholder="Search…">
<button class="btn">Search</button>
</div>Radio segmented — for single-select, .segmented uses native radios (CSS-only,
keyboard-accessible, form-submittable); the checked pill is skin-aware.
<div class="segmented" role="radiogroup">
<label class="segmented__option"><input type="radio" name="v" checked><span>Day</span></label>
<label class="segmented__option"><input type="radio" name="v"><span>Week</span></label>
</div>Table — .table
Transparent data table (place in a .card). .table--zebra for stripes, .table--compact for tighter rows.
<table class="table table--zebra"> … </table>Progress — .progress · Spinner — .spinner
| Class | Effect |
|-------|--------|
| .progress / .progress__bar | Track / fill (set width inline) |
| .progress--success / --warning / --error | Fill colour |
| .spinner | Loading spinner · --sm / --lg |
<div class="progress"><span class="progress__bar" style="width:72%"></span></div>
<span class="spinner"></span>Tooltip — .tooltip
Pure-CSS tooltip on hover/focus, inverted vs. the theme. .tooltip--bottom to flip placement.
<span class="tooltip">
<button class="btn">Hover me</button>
<span class="tooltip__text">Helpful hint</span>
</span>Interactive components (Modal, Dropdown)
Stateful components keep their state in the DOM as data-state="open|closed"; CSS
renders it. Nothing in the contract is tied to the library name.
- Vanilla — load the optional behaviour layer; it auto-wires triggers and adds
scroll-lock, focus-trap, Esc and outside/backdrop close:
Triggers:<script type="module" src="src/js/interactions.auto.js"></script>data-open="#id",data-close,data-toggle. (Prefix them viaconfigure({ prefix: 'ui' })→data-ui-open.) - React / Vue / anything — no library JS needed. Bind
data-stateto your own state and handle clicks your way. Optionally import pure helpers:import { open, close, trapFocus, lockScroll } from './src/js/interactions.js'.
Modal — .modal-overlay + .modal
Mark the overlay data-overlay so the JS applies scroll-lock + focus-trap + backdrop-close.
<button class="btn" data-open="#dlg">Open</button>
<div class="modal-overlay" id="dlg" data-state="closed" data-overlay>
<div class="modal" role="dialog" aria-modal="true">
<h3 class="modal__title">Title</h3>
<p class="modal__body">Body…</p>
<div class="modal__actions"><button class="btn btn--ghost" data-close>Close</button></div>
</div>
</div>Dropdown — .dropdown
<div class="dropdown" data-state="closed">
<button class="btn" data-toggle>Menu <span class="dropdown__caret"></span></button>
<div class="dropdown__menu">
<a class="dropdown__item" href="#">Profile</a>
<div class="dropdown__divider"></div>
<a class="dropdown__item" href="#">Sign out</a>
</div>
</div>Avatar — .avatar
Image or initials, with --sm/--lg/--square, an .avatar__status dot, and an
overlapping .avatar-group.
Accordion — .accordion
Built on native <details> — zero JS. Add name="…" to items to make them exclusive.
<div class="accordion">
<details class="accordion__item" name="faq" open>
<summary class="accordion__head">Question</summary>
<div class="accordion__body">Answer…</div>
</details>
</div>Toast — .toast
Show imperatively with the JS helper (auto-creates the container, auto-dismisses):
import { toast } from './dist/interactions.mjs';
toast('Changes saved.', { type: 'success', timeout: 4000 });Pagination — .pagination
.pagination__item for pages/arrows; --active (skin-aware) / --disabled.
Variants: .pagination--joined (button-group style, shared borders) · .pagination--sm.
Use .pagination__ellipsis for … gaps in long ranges.
Distribution
npm run build produces everything into dist/ — Sass compiles the CSS (expanded +
minified per skin), PostCSS/autoprefixer adds vendor prefixes (targets from the
browserslist field), and esbuild bundles the JS.
CSS — one file per skin, expanded and minified:
| Skin | Expanded | Minified |
|------|----------|----------|
| glass (default) | dist/aura.css | dist/aura.min.css |
| flat | dist/aura-flat.css | dist/aura-flat.min.css |
| neu | dist/aura-neu.css | dist/aura-neu.min.css |
JS (optional behaviour layer):
| File | Use |
|------|-----|
| dist/interactions.mjs / .min.mjs | ESM — import { open, toast } from '@oleksandr-94/aura-css' (frameworks, <script type=module>) |
| dist/interactions.global.min.js | Minified IIFE — <script src> exposes window.Aura + auto-wires triggers |
| dist/interactions.d.ts | TypeScript types |
The global name is esbuild's --global-name flag — rename freely; the DOM contract is
unaffected.
Package entry points (exports):
import { open, toast } from '@oleksandr-94/aura-css'; // JS (ESM) + types
import '@oleksandr-94/aura-css/css'; // glass CSS (also: @oleksandr-94/aura-css/css/min)
import '@oleksandr-94/aura-css/flat'; // flat skin (also: @oleksandr-94/aura-css/flat/min)
import '@oleksandr-94/aura-css/neu'; // neu skin
// SCSS source for custom builds: @use '@oleksandr-94/aura-css/scss' with ($skin: flat, $prefix: 'ui');Accessibility
- Reduced motion — Aura component transitions/animations are neutralised under
prefers-reduced-motion: reduce(in the last cascade layer, so it never touches the host app's own motion). - Keyboard — Modal: focus-trap, Esc, focus return. Dropdown:
↑/↓/Home/Endover items, Esc,aria-expanded, focus return. Tabs:←/→roving focus. - Screen readers — Toasts announce via an
aria-live="polite"region (errors userole="alert"); a.sr-onlyhelper is provided for visually-hidden labels. - Focus visible — every interactive component has a
:focus-visiblering.
Browser support
Aura targets an evergreen baseline (Chrome/Edge 111+, Firefox 113+, Safari 16.4+), which is required for:
| Feature | Used for | If missing |
|---------|----------|------------|
| color-mix() | tints, hovers, neu shadows | no practical polyfill — hard requirement |
| Cascade layers (@layer) | override control | hard requirement |
| backdrop-filter | glass skin only | glass loses blur (film stays); use flat/neu |
These features are the same generation, so there is no meaningful fallback to add — the
baseline is documented rather than polyfilled. Non-glass skins (flat, neu) have the
lightest requirements.
Architecture
aura/
├── src/ # library source
│ ├── _config.scss # the 3 levers: $prefix, $skin, $themes
│ ├── _functions.scss # auto on-colour (contrast) derivation
│ ├── _tokens.scss # emits static scales + generates every theme
│ ├── _surface.scss # skin dispatcher: surface() / field()
│ ├── skins/ # _glass · _flat · _neu
│ ├── base/_reset.scss # minimal reset in @layer base
│ ├── components/ # _button · _card
│ ├── _layers.scss # cascade-layer order
│ └── index.scss # entry (default = glass)
├── builds/ # build presets (recipes): flat.scss …
├── dist/ # compiled CSS output
├── demo/ # live demo page
└── package.json- Cascade layers — all Aura CSS lives in
@layer base, components; your app's own unlayered styles always win, so overriding is easy. - Coexistence — set
$prefixto avoid class clashes with other libraries. With Tailwind v4 you can also map Aura's role variables into@themeto share one palette.
Testing
Unit tests cover the JS behaviour layer (interactions.js) with Vitest + happy-dom:
npm test # run once
npm run test:watchCovered: open/close/toggle, mount() trigger delegation, activateTab, ref-counted
scroll-lock, toast() lifecycle, and prefix config.
Not yet (roadmap)
- Components:
breadcrumbs,menu/sidebar,steps,skeleton,stat,drawer,popover. - More prebuilt themes (the token system supports them cheaply).
- Visual-regression tests (Playwright over the docs, per skin × theme) + a11y audit (axe).
- RTL audit ·
prefers-color-schemeauto-theme · per-component CSS imports (tree-shaking).
