12 — Components

Tabs

Auto-init via data-tabs. Arrow keys ←/→ to navigate.

Overview

downstage.css is a minimal design system inspired by Nordic aesthetics. Built for developers who want something clean without learning a utility-first library.

Installation

Download the files, copy them to your public folder, include the CSS in your layout. Add downstage.js for built-in interactive components: tabs, accordion, lightbox, navbar hamburger, and more.

Configuration

All variables are customizable. Override tokens in a file after downstage.css to change colors, spacing, fonts.

JavaScript API

Exposes window.Downstage with modules: theme, navbar, tabs, accordion, lightbox, htmlEditor, and more. Each has an init() method called automatically on DOMContentLoaded.

<div class="tabs" data-tabs>
  <ul class="tabs-list" role="tablist">
    <li><button class="tab" role="tab" data-tab="t1">Overview</button></li>
    <li><button class="tab" role="tab" data-tab="t2">Installation</button></li>
    <li><button class="tab" role="tab" data-tab="t3">Configuration</button></li>
  </ul>

  <div class="tab-panel" data-tab-panel="t1">
    <h4>Overview</h4>
    <p>Panel content here.</p>
  </div>
  <div class="tab-panel" data-tab-panel="t2">
    <h4>Installation</h4>
    <p>Panel content here.</p>
  </div>
  <div class="tab-panel" data-tab-panel="t3">
    <h4>Configuration</h4>
    <p>Panel content here.</p>
  </div>
</div>

Root: data-tabs on .tabs. Tab buttons: data-tab="id" (unique per set). Panels: data-tab-panel="id" matching the active tab. Optional: class="active" on a tab for initial selection.

// Wire a single root after dynamic insert:
Downstage.tabs.mount(document.querySelector('#my-tabs'));

// Or re-run auto-init for the whole document:
Downstage.tabs.init();

13 — Components

Accordion

Add data-accordion="single" for exclusive mode.

Yes. Place the files in public/css/ and include the CSS in your Blade layout with {{ asset('css/downstage.css') }}. Semantic classes are great in Blade templates — much more readable than utility-first.
Almost everything: layout, typography, buttons, forms, alerts, badges, tables, cards. You only need the ~3 KB downstage.js for: mobile navbar, tabs, accordion, lightbox, theme switcher, and the HTML editor toolbar.
Everything is a CSS variable. Create a theme.css file after downstage.css and redefine the tokens you need in :root for light and [data-theme="dark"] for dark.
Yes, they are custom and free to use. Minimal stroke style at 1.6px, 100+ icons, all in a single SVG sprite of a few KB. Loaded once and reused via <use>.
<div class="accordion" data-accordion="single">
  <div class="accordion-item is-open">
    <button class="accordion-header">
      Question text
      <svg class="icon accordion-icon">
        <use href="downstage-icons.svg#plus" />
      </svg>
    </button>
    <div class="accordion-content">
      <div class="accordion-body">
        Answer content here.
      </div>
    </div>
  </div>

  <div class="accordion-item">
    <button class="accordion-header">
      Another question
      <svg class="icon accordion-icon">
        <use href="downstage-icons.svg#plus" />
      </svg>
    </button>
    <div class="accordion-content">
      <div class="accordion-body">
        Answer content here.
      </div>
    </div>
  </div>
</div>
Downstage.accordion.init();        // all [data-accordion] on the page
Downstage.accordion.mount(el);    // single container

Exclusive mode: data-accordion="single" on the root (only one .accordion-item.is-open at a time).

14 — Components

Cascading selects

Two patterns for dependent <select> chains. Local reads a nested hierarchy (Region → Province → City) from an embedded JSON block—no network round trip. Remote uses the public JSONPlaceholder API (User → Post → Comment): each step issues a real fetch to jsonplaceholder.typicode.com over HTTPS with CORS enabled for browsers.

Local data

The hierarchy is parsed from a <script type="application/json"> node in the document—no HTTP request. Use this approach for small static datasets or values inlined at build time.

Remote API

JSONPlaceholder serves sample REST resources over HTTPS with CORS. This demo performs: (1) GET /users on load, (2) GET /posts?userId=… after a user is selected, (3) GET /comments?postId=… after a post is selected—three distinct requests to jsonplaceholder.typicode.com. In DevTools, open Network → Fetch/XHR to inspect traffic. Reload users repeats only the first request.

Enable Preserve log, then step through User → Post → Comment to observe each call.

<script type="application/json" id="demo-cascade-geo-local-data">
{ "regions": [ … ] }
</script>

<div id="demo-cascade-local" class="stack">
  <div class="field">
    <label class="label" for="region">Region</label>
    <select id="region" class="select" data-cascade-region></select>
  </div>
  <div class="field">
    <label class="label" for="province">Province</label>
    <select id="province" class="select" data-cascade-province></select>
  </div>
  <div class="field">
    <label class="label" for="city">City</label>
    <select id="city" class="select" data-cascade-city></select>
  </div>
</div>

Wiring is implemented in demo/demo.js (wireCascadingGeoSelects) by reading JSON from the script block.

<p id="demo-cascade-remote-status" role="status"></p>
<button type="button" id="demo-cascade-remote-refetch">Reload users</button>

<div id="demo-cascade-remote" class="stack">
  <div class="field">
    <label class="label" for="u">User</label>
    <select id="u" class="select" data-jp-user></select>
  </div>
  <div class="field">
    <label class="label" for="p">Post</label>
    <select id="p" class="select" data-jp-post></select>
  </div>
  <div class="field">
    <label class="label" for="c">Comment</label>
    <select id="c" class="select" data-jp-comment></select>
  </div>
</div>

Handlers live in demo/demo.js (initJsonPlaceholderCascading).

Uses standard form primitives from downstage.css: .stack (vertical spacing), .field, .label, .select. No extra stylesheet is required for this demo.

{
  "regions": [
    {
      "id": "ne",
      "name": "Northeast",
      "provinces": [
        {
          "id": "ny",
          "name": "New York",
          "cities": [
            { "id": "nyc", "name": "New York City" }
          ]
        }
      ]
    }
  ]
}
var base = 'https://jsonplaceholder.typicode.com';

fetch(base + '/users').then(r => r.json());           // 1 — fill users
fetch(base + '/posts?userId=' + id).then(r => r.json()); // 2 — after user
fetch(base + '/comments?postId=' + id).then(r => r.json()); // 3 — after post

16 — Components

Slideover

A panel that slides in from the right (useful for quick edits, filters, or mobile-friendly drawers). Structure: .slideover-overlay (full-screen backdrop) + .slideover-panel with .slideover-header, .slideover-body, optional .slideover-footer.

<div id="my-slideover" class="slideover-overlay hidden" aria-hidden="true">
  <aside class="slideover-panel" role="dialog" aria-labelledby="so-title">
    <div class="slideover-header">
      <h2 class="slideover-title" id="so-title">Filters</h2>
      <button type="button" class="btn btn-ghost btn-sm" data-slideover-close aria-label="Close">×</button>
    </div>
    <div class="slideover-body">…</div>
    <div class="slideover-footer">
      <button type="button" class="btn btn-primary">Apply</button>
    </div>
  </aside>
</div>

No dedicated Downstage.slideover module — toggle .hidden on the overlay and sync aria-hidden. Close targets use [data-slideover-close].

var el = document.getElementById('demo-slideover');
document.getElementById('demo-slideover-open').addEventListener('click', function () {
  el.classList.remove('hidden');
  el.setAttribute('aria-hidden', 'false');
  document.body.style.overflow = 'hidden';
});
el.querySelectorAll('[data-slideover-close]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    el.classList.add('hidden');
    el.setAttribute('aria-hidden', 'true');
    document.body.style.overflow = '';
  });
});

Reference implementation: demo/demo.js.

18 — Components

Wizard

Multi-step flow with optional step navigation and footer actions. Mark up a root .wizard with data-wizard, one .wizard-panel for each step (order = DOM order), nav buttons with data-wizard-goto (0-based index), and data-wizard-prev / data-wizard-next. Linear (default): users can only open steps they have already reached. Free (data-wizard-free="true"): any step can be opened from the nav. Labels come from i18n (wizard.*); optional [data-wizard-progress] shows “Step n of m”. Initialize with Downstage.wizard.mount(el, options) or rely on auto-init.

Default (linear)

Numbered pills, previous/next, progress text. Later steps in the nav stay disabled until you advance with Next.

New project

Name and visibility for the project.

Vertical layout + dots (free navigation)

Add .wizard--vertical and .wizard-nav--dots on the nav list. Set data-wizard-free="true" so every step is available from the nav. Dot buttons use a visually hidden label for screen readers.

Cart contents and quantities.

<div class="wizard" data-wizard>
  <ol class="wizard-nav" role="tablist" aria-label="Wizard steps">
    <li>
      <button type="button" class="wizard-nav-item" data-wizard-goto="0" role="tab">
        <span class="wizard-nav-num" aria-hidden="true">1</span> Basics
      </button>
    </li>
    <li>…</li>
  </ol>
  <div class="wizard-panels">
    <section class="wizard-panel">…</section>
    <section class="wizard-panel" hidden>…</section>
  </div>
  <div class="wizard-footer">
    <span class="wizard-meta" data-wizard-progress></span>
    <button type="button" class="btn btn-ghost" data-wizard-prev>Previous</button>
    <button type="button" class="btn btn-primary" data-wizard-next>Next</button>
  </div>
</div>

<!-- Free navigation (any step from nav) -->
<div class="wizard wizard--vertical" data-wizard data-wizard-free="true"> … </div>
Downstage.wizard.mount(document.querySelector('#my-wizard'), {
  free: false,
  onStepChange: function (step, total) { console.log(step, total); },
  onComplete: function () { console.log('done'); }
});

// API returned by mount:
// .goToStep(0), .next(), .prev(), .getStep(), .getTotal()
Attribute / optionDescription
data-wizard Root marker; required for auto-init.
data-wizard-free="true" Allow any step from the nav (default linear progression).
data-wizard-goto="n" 0-based index on nav buttons.
data-wizard-prev / data-wizard-next Footer controls; Next becomes “Finish” on the last step.
data-wizard-progress Container filled with i18n “Step n of m”.
options.free Same as data-wizard-free when calling mount.

04 — Reference

JavaScript API

This page focuses on interactive components (tabs, accordion, wizard, …). The canonical reference for every Downstage.* module, global configuration, and links to detailed option tables lives under Basic → JavaScript API (basic.html#doc-api).

Installation

Download the framework files and place them in your project's public directory. The fonts/ folder must sit alongside the CSS file.

<link rel="stylesheet" href="downstage.css">
<script src="downstage.js" defer></script>

That's it. No build tools, no configuration files, no package managers required. Just two files and you're ready.

Configuration

All visual properties are controlled by CSS custom properties (variables). Override them in a separate file loaded after downstage.css:

:root {
  --brand-primary: #2c5f5d;
  --radius: 2px;
  --container: 800px;
}
  • Colors — neutral scale, brand, semantic
  • Typography — font families, sizes, weights
  • Spacing — 4px-based scale from 0.25rem to 6rem
  • Layout — container widths, grid gaps

Theming

Set data-theme on the <html> element. Three modes are supported:

  1. light — always light
  2. dark — always dark
  3. auto — follows prefers-color-scheme

The dark theme overrides all neutral, brand, and semantic tokens. Shadows become darker, surfaces shift to warm charcoals, and text becomes light cream.

JavaScript API

See Global configuration and the full module index on the Basic page. Interactive UI components on this page map to:

  • Downstage.tabsTabs
  • Downstage.accordionAccordion
  • Downstage.wizardWizard
  • Downstage.formValidate — used with chained modals / slideover forms; full options on Form validation

Modal and slideover patterns here are CSS + small scripts in demo/demo.js; there is no separate overlay framework API.