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.
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.
downstage.js for: mobile navbar, tabs, accordion, lightbox, theme switcher, and the HTML
editor toolbar.
theme.css file after downstage.css and redefine the
tokens you need in :root for light and [data-theme="dark"] for dark.
<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
15 — Components
Modal
Click the button to open the dialog.
<button class="btn btn-danger"
onclick="document.getElementById('demo-modal').classList.remove('hidden')">
<svg class="icon"><use href="downstage-icons.svg#trash" /></svg>
Delete entry
</button>
<div id="demo-modal" class="modal-overlay hidden"
onclick="if(event.target===this)this.classList.add('hidden')">
<div class="modal">
<div class="modal-title">Delete Entry</div>
<div class="modal-body">Are you sure? This action is irreversible.</div>
<div class="modal-footer">
<button class="btn btn-secondary"
onclick="document.getElementById('demo-modal').classList.add('hidden')">Cancel</button>
<button class="btn btn-primary"
onclick="document.getElementById('demo-modal').classList.add('hidden')">Confirm</button>
</div>
</div>
</div>
Chained modals
Confirm first, then open a second dialog with a form (e.g. “Are you sure?” → billing details). The second dialog uses data-form-validate on its inner form.
<button type="button" id="demo-modal-chain-open">Start chain…</button>
<!-- 1) Confirm -->
<div id="demo-modal-chain-confirm" class="modal-overlay hidden"
onclick="if(event.target===this)this.classList.add('hidden')">
<div class="modal" role="dialog" aria-modal="true" onclick="event.stopPropagation()">
<div class="modal-title">Continue?</div>
<div class="modal-body">…</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-chain-close>Cancel</button>
<button type="button" class="btn btn-primary" data-chain-continue>Continue</button>
</div>
</div>
</div>
<!-- 2) Form (add data-form-validate on form for Downstage.formValidate) -->
<div id="demo-modal-chain-form" class="modal-overlay hidden"
onclick="if(event.target===this)this.classList.add('hidden')">
<div class="modal" role="dialog" aria-modal="true" onclick="event.stopPropagation()">
<div class="modal-title">Billing details</div>
<div class="modal-body">
<form id="demo-form-chain" novalidate>
<div class="field">
<label class="label" for="line1">Address</label>
<input id="line1" class="input" name="line1" required>
<span class="error" aria-live="polite"></span>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-chain-close>Cancel</button>
<button type="submit" class="btn btn-primary" form="demo-form-chain">Save</button>
</div>
</div>
</div>
Open the first overlay, then swap to the second on Continue; wire
data-chain-close / data-chain-continue.
Full logic: demo/demo.js (search for demo-modal-chain).
document.getElementById('demo-modal-chain-open')
.addEventListener('click', function () {
document.getElementById('demo-modal-chain-confirm').classList.remove('hidden');
});
// Continue → hide confirm, show form modal
// Cancel → .classList.add('hidden') on active overlay
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.
Add metadata; this step is only reachable after step 1 in linear mode.
Confirm and submit. Finish runs the completion callback (see JS accordion below).
In your app you would POST the form or route to the next screen.
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.
Address and delivery options.
Payment method and review.
<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 / option | Description |
|---|---|
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:
light— always lightdark— always darkauto— followsprefers-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.tabs— TabsDownstage.accordion— AccordionDownstage.wizard— WizardDownstage.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.