npm.io
0.1.0 • Published yesterday

@oleksandr-94/aura-css

Licence
MIT
Version
0.1.0
Deps
0
Size
311 kB
Vulns
0
Weekly
0

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 + types

Include 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="you@studio.dev">
</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:
    <script type="module" src="src/js/interactions.auto.js"></script>
    Triggers: data-open="#id", data-close, data-toggle. (Prefix them via configure({ prefix: 'ui' })data-ui-open.)
  • React / Vue / anything — no library JS needed. Bind data-state to 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/End over items, Esc, aria-expanded, focus return. Tabs: / roving focus.
  • Screen readers — Toasts announce via an aria-live="polite" region (errors use role="alert"); a .sr-only helper is provided for visually-hidden labels.
  • Focus visible — every interactive component has a :focus-visible ring.

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 $prefix to avoid class clashes with other libraries. With Tailwind v4 you can also map Aura's role variables into @theme to share one palette.

Testing

Unit tests cover the JS behaviour layer (interactions.js) with Vitest + happy-dom:

npm test          # run once
npm run test:watch

Covered: 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-scheme auto-theme · per-component CSS imports (tree-shaking).

Keywords