downstage
v1.0.2
Published
Minimal Nordic design system — CSS, zero-dependency JS components, icon sprite
Maintainers
Readme
downstage.css
Minimal Nordic design system — lightweight, zero dependencies, for sites and web apps where content comes before ornament. downstage.js adds JavaScript with built-in components (tabs, lightbox, HTML editor, Kanban, data table, order list, i18n, and more).
Clean typography, warm desaturated palette, thin borders, light shadows, integrated dark mode.
Table of contents
- File structure
- npm
- Quick start
- Theme
- Design tokens (CSS variables)
- Typography & text utilities
- Icons
- Layout
- Components
- Utilities
- JavaScript (
downstage.js) - HTML editor (detailed)
- Documentation site
- Philosophy
- Customization
- AI assistants
- License
File structure
your-project/
├── downstage.css ← styles (required)
├── downstage.js ← JS with built-in components (~few KB)
├── downstage-icons.svg ← 170+ icon sprite (stroke icons)
├── fonts/ ← Space Grotesk (self-hosted)
├── docs/ ← documentation site (multi-page gallery)
│ ├── index.html ← hub listing every topic page
│ ├── getting-started.html, icons.html, foundation.html, …
│ └── (generated from `catalog-full.html` via `demo/split_catalog.py`)
├── demo/ ← demo-only assets (not required in your app)
│ ├── demo.css ← demo + docs layout helpers
│ ├── docs.css ← documentation site chrome
│ ├── demo.js ← combobox/search/table demos
│ ├── split_catalog.py ← rebuild `docs/*.html` from `catalog-full.html`
│ └── kanban-board.json ← sample Kanban payload
├── catalog-full.html ← optional single-file snapshot of all components
├── downstage-ai-guidelines.json ← machine-readable guidelines for AI tools (Cursor, MCP scripts, …)
└── index.html ← home; start at `docs/index.html` for the full gallerynpm
Published as downstage.
npm install downstageAfter install, assets live under node_modules/downstage/:
| File / folder | Role |
| ------------- | ---- |
| downstage.css | Styles (package.json → style) |
| downstage.js | Components and window.Downstage (main) |
| downstage-icons.svg | Icon sprite |
| fonts/ | Space Grotesk (self-hosted) |
| downstage-ai-guidelines.json | Conventions for AI-assisted editing |
Static sites: copy downstage.css, downstage.js, downstage-icons.svg, and fonts/ into your public or static directory, then reference them with normal relative URLs (same as a vendored download).
Bundlers: import the CSS and JS from the package:
import "downstage/downstage.css";
import "downstage/downstage.js";Upgrade: npm update downstage, or set the version in package.json and run npm install.
Publishing (maintainers): npm version patch (or minor / major), then npm publish (requires npm login to the npm account that owns the package).
Quick start
<!DOCTYPE html>
<html lang="en" data-theme="auto">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="downstage.css" />
<script src="downstage.js" defer></script>
</head>
<body>
<main class="container">
<h1>Hello world</h1>
<button class="btn btn-primary">
<svg class="icon"><use href="downstage-icons.svg#rocket" /></svg>
Launch
</button>
</main>
</body>
</html>Layout and styling work from CSS alone; downstage.js layers built-in interactive components (theme persistence, navbar, tabs, accordion, lightbox, HTML editor, combobox, Kanban, data table, order list, and more).
Theme (light / dark / auto)
Set data-theme on <html> to light, dark, or auto (follows prefers-color-scheme).
Wire theme buttons (any element):
<button type="button" data-set-theme="light">Light</button>
<button type="button" data-set-theme="dark">Dark</button>
<button type="button" data-set-theme="auto">Auto</button>With downstage.js, clicks persist the choice under localStorage key downstage-theme, sync <html data-theme>, and toggle btn-primary on the active control.
API: Downstage.theme.get() → 'light' | 'dark' | 'auto' (falls back to stored or 'light'). Downstage.theme.set('dark') applies immediately.
Design tokens (CSS variables)
| Token | Role |
| ----- | ---- |
| --bg, --bg-elevated, --bg-sunken, --bg-hover | Surfaces |
| --text, --text-soft, --text-muted, --text-subtle | Text hierarchy |
| --border, --border-strong | Hairline borders |
| --link, --link-hover | Links |
| --brand-primary, --brand-secondary | Brand (sage / taupe) |
| --color-success, --color-danger, --color-warning, --color-info | Semantic |
| --space-1 … --space-24 | Spacing (4–96px scale) |
| --radius, --radius-sm, --radius-lg | Corners |
| --fs-xs … --fs-3xl, --lh-*, --fw-* | Type scale |
| --container, --container-narrow, --container-wide | Max widths |
| --shadow, --shadow-lg | Shadows |
| --z-modal, … | Stacking |
Override in a stylesheet after downstage.css (see Customization).
Typography & text utilities
Semantic headings h1–h6 use the type scale. Utility classes:
- Size / tone:
.text-xs,.text-sm,.text-muted,.text-soft,.text-subtle - Font:
.text-mono(tabular figures where relevant) - Alignment:
.text-left,.text-center,.text-right - Spacing helpers:
.mt-*,.mb-*(0, 2, 4, 6, 8, 10, 12, 16) - Screen readers:
.sr-only(visually hidden, available to AT)
Code: code, kbd, pre are styled in the base sheet.
Icons
SVG sprite (downstage-icons.svg) with 170+ stroke icons; color follows currentColor.
<svg class="icon" aria-hidden="true"><use href="downstage-icons.svg#rocket" /></svg>Size classes: .icon-sm, .icon (default), .icon-lg, .icon-xl, .icon-2xl.
Families: navigation, actions, files, comms, users, dev/tech, media, status, time/place, misc. The docs/icons.html page lists symbol ids.
Layout
| Utility | Description |
| ------- | ----------- |
| .container / .container-narrow / .container-wide | Centered horizontal padding + max-width |
| .stack / .stack-sm / .stack-lg | Vertical flex column + gap |
| .cluster | Horizontal flex, wrap, gap (actions, tags) |
| .grid | 12-column grid; children .col-1 … .col-12 |
| .grid-auto | Auto-fit minmax column grid (where used in demos) |
The grid collapses for small viewports (see breakpoints in CSS — mobile-first around 768px for nav).
Components
Below: class names, required markup hooks, and downstage.js behavior where applicable. The docs/ site is the canonical visual reference for every block (see docs/index.html).
Buttons
.btn + variant: .btn-primary, .btn-secondary, .btn-ghost, .btn-danger, .btn-danger-solid, .btn-success. Modifiers: .btn-sm, .btn-lg, .btn-block. Use on <button> or <a>.
Forms
- Field:
.fieldwrapping.label+.input/.textarea/.select - Password:
<input type="password" class="input">—downstage.jswraps it with an eye toggle (show / hide). Usedata-password-toggle="off"to disable. Labels:password.show/password.hideinlocales/*.json. - Copy: wrap a readonly
textareaor disabledinputin.input-copy-wrapwith abutton.input-copy-btn(optionalspan.input-copy-btn-label). Add.input-copy-wrap-minimalwithtextarea-minimal/input-minimalfor underline-on-background styling. StringscopyField.copy/copyField.copied. - States:
.field.has-error,.error,.help - Controls:
.check(checkbox),.switch+.switch-slider - Minimal:
.input-minimal,.select-minimal(underline style)
Navbar
.navbar > .navbar-inner > brand, .navbar-toggle (three spans), .navbar-menu with .navbar-link, optional .navbar-actions, optional details.navbar-dropdown with .navbar-dropdown-link.
JS: toggles .is-open, updates aria-expanded, closes on link click / Esc / resize to desktop; locks body scroll on small viewports when open.
Breadcrumb
ol.breadcrumb with li + links; first item often a home icon.
Tabs
<div class="tabs" data-tabs>
<ul class="tabs-list" role="tablist">…</ul>
<div class="tab-panel" data-tab-panel="id">…</div>
</div>Buttons use role="tab", data-tab="id", class .tab (active state .active). Panels: data-tab-panel="id".
JS: [data-tabs] — activates panels; ← / → move focus between tabs.
Accordion
.accordion with [data-accordion="single"] (only one open) or omit for independent items. Structure: .accordion-item > .accordion-header + .accordion-content > .accordion-body. Open state: .accordion-item.is-open.
Table
.table-wrap (scroll / focus) > table.table. Variants: .table-minimal, .table-compact, .table-striped (combinable).
Card, alert, badge
- Card:
.card,.card-header,.card-footer - Alert:
.alert+.alert-info|.alert-success|.alert-warning|.alert-danger - Badge:
.badge+ same semantic suffixes
Modal
.modal-overlay (fixed backdrop) > .modal with .modal-title, .modal-body, .modal-footer. Toggle visibility with .hidden on the overlay. Click backdrop to close is optional (see demo).
Image frame
.image-frame + ratio class .is-square, .is-4-5, .is-16-9, .is-3-2 > .image-media > img, optional .image-caption.
Gallery + lightbox
.gallery (optional .gallery-2 / 3 / 4) + [data-lightbox="unique-set-name"]. Items: a.gallery-item pointing to full-size href, thumbnail img inside.
JS: builds a single .lightbox at end of body; Downstage.lightbox.open(arrayOf { src, alt }, index) and close(). Clicks on gallery items call open with the derived set. Keys: Esc, ← / →.
Slider / carousel
<div class="slider" data-slider>
<div class="slider-track">
<div class="slider-slide">…</div>
</div>
</div>Attributes: data-slider-dots, data-slider-arrows, data-slider-auto="5000" (ms). Optional markup: .slider-prev, .slider-next, .slider-dots > .slider-dot (generated if missing when dots/arrows requested).
Video player
<div class="video-player" data-video-player>
<video src="…"></video>
</div>JS: injects controls (play, progress, time, mute, fullscreen), removes native controls attribute.
Audio player
<div class="audio-player" data-audio-player data-src="file.mp3" data-title="Title" data-artist="Artist"></div>JS: builds minimal UI if empty; uses Web Audio Audio element.
File upload (drag & drop)
Root [data-upload-drop] must contain:
input[type=file](often class.sr-only).upload-drop-zone- optional
ul.upload-drop-listfor filenames
Merges dropped files into the input; click on zone opens file dialog.
HTML editor
App UI (auth, combobox, Kanban, data table, order list)
Markup plus downstage.js built-in behaviors for sign-in flows, autocomplete fields, boards, tables, and order/invoice builders. See docs/apps.html and docs/data.html.
Authentication shells
Centered .auth-shell (optional min-height) wraps .auth-card (max-width, padding). Use .auth-brand, .auth-title, .auth-subtitle, .auth-form, .auth-actions, .auth-footer, .auth-links-row. Wide variant: .auth-card-wide. .auth-otp holds one-character inputs for 2FA codes (pair with your own paste / validation logic).
Combobox (autocomplete select)
.combobox > .combobox-input-wrap > .combobox-input + .combobox-list > .combobox-option. States: .is-open, .is-active (keyboard highlight), .combobox-empty, .combobox-loading. Search-style icon: .combobox--search (icon + padded input).
Declarative: [data-combobox] with optional data-combobox-local='[{"value":"…","label":"…"},…]', data-combobox-fetch="/api?q=" (GET, JSON array or { results: [] }), data-combobox-placeholder, data-combobox-name, data-combobox-min-chars, data-combobox-debounce.
JS: Downstage.combobox.mount(el, options) — options: options (local array), fetchOptions(query) (async), fetchUrl (same-origin friendly), minChars, debounceMs, placeholder, name, value, prependIcon: 'search'. Emits combobox-change on the root when a value is chosen.
Search autocomplete
.search-autocomplete (optional .search-autocomplete--wide) wraps the same combobox pattern with a leading search icon. Downstage.searchAutocomplete.mount(el, options) — fetchSuggestions or fetchOptions / fetchUrl like the combobox.
Kanban
.kanban-board (horizontal scroll) > .kanban-column > header .kanban-column-header + .kanban-column-body > .kanban-card (draggable). Toolbar: .kanban-toolbar. Drop highlight: .kanban-column.is-drop-target.
JS — AJAX: Prefer fetchUrl + moveUrl (built-in fetch). fetchUrl — GET JSON shaped as { columns: [...] } (each column: id, title, cards: [{ id, title, meta? }]). moveUrl — POST (or moveMethod: PATCH, etc.) with JSON body { cardId, fromColumnId, toColumnId, toIndex }. Cross-origin moveUrl uses credentials: omit unless you set moveCredentials. Optional fetchCredentials for the board request.
Programmatic: Downstage.kanban.mount(el, { fetchUrl, moveUrl, fetchBoard, moveCard, initialColumns }) — custom fetchBoard / moveCard override URL-based helpers.
Declarative: data-kanban-fetch-url, data-kanban-move-url, data-kanban-move-method, data-kanban-move-credentials, data-kanban-fetch-credentials. Demo: [data-kanban][data-kanban-demo] loads demo/kanban-board.json and POSTs moves to https://httpbin.org/post (replace with your API).
Data table
.data-table-wrap > .data-table-toolbar (search) + .data-table-scroll > table.table.data-table + .data-table-footer with .data-table-pagination. Sortable headers: th.sortable + .sort-indicator. Loading: .data-table-wrap.is-loading.
JS: Downstage.dataTable.mount(el, options) — columns: [{ key, label, sortable? }], mode: 'local' | 'remote', rows (local), pageSize, fetchRemote({ page, pageSize, sortKey, sortDir, q }) → Promise<{ rows, total }>. Local demo: [data-data-table][data-data-table-demo].
File card
a.file-card with .file-card-icon, .file-card-body, .file-card-title, .file-card-meta.
Footer
.footer > .footer-inner > .footer-grid, .footer-title, .footer-links, .footer-bottom, .footer-social.
Page templates (see docs/templates.html and related pages)
| Anchor | Highlights |
| ------ | ---------- |
| #docs-demo | .docs-layout, .docs-sidebar, .docs-content, in-page .docs-nav |
| #dashboard-demo | .stat-card, charts, data density |
| #portfolio | .portfolio, .portfolio-item, .portfolio--minimal |
| #blog | .blog-list, .blog-card, minimal variants |
| #links | .link-list, .link-item (link-in-bio style) |
| #about | Split layout, .about-* |
| #profile | .profile-shell, .profile-block |
| #team | .team-grid, .team-card, .team-avatar |
| #timeline | ol.timeline, .timeline-item, .timeline--compact |
| #shop | .shop-grid, .shop-card, .product-detail, .cart, checkout / payment panels |
| docs/apps.html (auth blocks) | .auth-shell, .auth-card, sign in / sign up / recovery / 2FA patterns |
| docs/apps.html (data UI) | Combobox, search autocomplete, Kanban, data table, order list (JS-backed demos) |
| #filters | Image filter classes (see Utilities) |
Sidebar nav (settings-style)
nav.nav-vertical with a / .active — used beside modal in the demo.
Utilities
- Images:
.img-bw,.img-sepia,.img-muted,.img-warm; hover variants.img-bw-hover,.img-sepia-hover(hover clears filter). Seedocs/utilities.html. - Display / flex helpers: as in CSS (e.g. utility clusters in demos).
- Reduced motion:
prefers-reduced-motionrespected where transitions are used.
JavaScript (downstage.js)
Global: window.Downstage. Every module exposes init(), invoked once on DOMContentLoaded. After adding new DOM that uses a module, call Downstage.<name>.init() again (or only that module).
Downstage.theme
| Method | Description |
| ------ | ----------- |
| get() | Current theme (localStorage + <html data-theme>). |
| set('light' \| 'dark' \| 'auto') | Sets attribute, persists, updates [data-set-theme] button styles. |
| init() | Restores saved theme; binds [data-set-theme] buttons. |
Downstage.navbar
| Selector / behavior |
| --------------------- |
| .navbar with .navbar-toggle + .navbar-menu — hamburger, Esc, resize, link close. |
Downstage.tabs
| Selector | Description |
| -------- | ----------- |
| [data-tabs] | Binds .tab [data-tab] to .tab-panel [data-tab-panel]. |
Downstage.accordion
| Selector | Description |
| -------- | ----------- |
| [data-accordion] | Optional data-accordion="single" for exclusive panels. |
Downstage.lightbox
| Method | Description |
| ------ | ----------- |
| init() | Wires each [data-lightbox] gallery: builds { src, alt }[] from .gallery-item links. |
| open(set, index) | set: array of { src, alt }; index: starting slide. |
| close() | Hides overlay, restores body scroll. |
Downstage.slider
| Selector | Description |
| -------- | ----------- |
| [data-slider] | Requires .slider-track and .slider-slide children. |
Downstage.videoPlayer
| Selector | Description |
| -------- | ----------- |
| [data-video-player] | Wraps <video>; injects custom controls. |
Downstage.audioPlayer
| Selector | Description |
| -------- | ----------- |
| [data-audio-player] | Requires data-src; optional data-title, data-artist. |
Downstage.uploadDrop
| Selector | Description |
| -------- | ----------- |
| [data-upload-drop] | File input + drop zone + optional file list (see above). |
Downstage.htmlEditor
| Method / property | Description |
| ----------------- | ----------- |
| init() | Mounts [data-html-editor-mount] then wires legacy [data-html-editor] (without data-html-editor-mounted). |
| mount(target, options?) | target: element or CSS selector. Builds UI, then runs internal setup. Returns the root element or null. |
| presets | Built-in preset objects (e.g. demo) for copy + defaults. |
Downstage.combobox
| Method / selector | Description |
| ----------------- | ----------- |
| init() | Mounts [data-combobox] (skips [data-search-autocomplete]). |
| mount(el, options) | Builds combobox; see App UI. Returns { input, hidden, root }. |
Downstage.searchAutocomplete
| Method / selector | Description |
| ----------------- | ----------- |
| init() | Mounts [data-search-autocomplete] with data-search-fetch, data-search-placeholder, etc. |
| mount(el, options) | Same as combobox with search icon (prependIcon internally). |
Downstage.kanban
| Method / selector | Description |
| ----------------- | ----------- |
| init() | Mounts [data-kanban]; [data-kanban-demo] uses demo/kanban-board.json + HTTP POST for moves (see README). |
| mount(el, options) | fetchUrl / moveUrl (AJAX) or fetchBoard / moveCard functions; optional initialColumns. Returns { refresh }. |
Downstage.dataTable
| Method / selector | Description |
| ----------------- | ----------- |
| init() | Mounts [data-data-table][data-data-table-demo] with sample rows. |
| mount(el, options) | Local or remote table; fetchRemote for server-driven pagination/sort/filter. Returns { refresh }. |
Downstage.orderList
Search-to-add item list for invoices, orders, and quote builders. Autocomplete search (local or API), configurable columns with quantity controls and computed values, column sort / drag-reorder, JSON output, optional create-via-modal.
| Method / selector | Description |
| ----------------- | ----------- |
| init() | Mounts [data-order-list] (skips [data-order-list-demo]). |
| mount(el, options) | Returns { root, getItems, setItems, addItem, removeItem, refresh }. |
Mount options:
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| name | string | "order_items" | Hidden input name for form submission. |
| columns | array | [] | Column definitions (see below). |
| options | array | [] | Local items: [{ value, label }]. value can be a string or object. |
| fetchUrl | string | — | GET endpoint; appends ?q=…, expects JSON array or { results } / { items }. |
| fetchOptions | function | — | Custom: function(query) → Promise<[{ value, label }]>. |
| itemKey | string | — | Dot-path into value for duplicate detection (e.g. "codice"). |
| items | array | [] | Pre-populated rows: [{ value, qty?, sort_order? }]. |
| sortMode | string | "column" | Initial mode: "column" (header click) or "manual" (drag reorder). |
| placeholder | string | i18n | Search input placeholder. |
| minChars | number | 0 local / 1 remote | Min characters before search fires. |
| debounceMs | number | 250 | Search debounce delay (ms). |
| onChange | function | — | function(items) — fired on every change. |
| createTitle | string | i18n | Modal heading for create form. |
| createFields | array | — | Modal field definitions (see below). Required to enable creation. |
| createOption | function | — | function(data) → Promise<{ value, label }>. Receives form data, returns the new item. |
| createUrl | string | — | POST JSON to this URL instead of createOption. |
Column definition:
| Property | Type | Description |
| -------- | ---- | ----------- |
| key | string | Unique column id (used as sort key). |
| label | string | Header text. |
| from | string | Dot-path resolved on the row, e.g. "value.codice". |
| type | string | "qty" renders +/− controls. One qty column per list. |
| compute | function | function(item) → displayValue. Not sortable by header. |
| render | function | function(cellValue, item) → html. Custom cell HTML. |
| sortable | boolean | false to disable header sort (auto-disabled for qty / computed). |
Create field definition:
| Property | Type | Description |
| -------- | ---- | ----------- |
| key | string | Property name in submitted data. |
| label | string | Field label. |
| type | string | "text", "number", "select", or "textarea". |
| options | array | For select: strings or { value, label }. |
| required | boolean | Block submit when empty. |
| placeholder | string | Input placeholder. |
| default | any | Default value on open. |
| step | string | Step for number inputs (e.g. "0.01"). |
Event: order-list-change (bubbling CustomEvent) — event.detail.items is the full JSON array.
Output format:
[
{
"value": { "codice": "P001", "nome": "Widget Pro", "unita": "pz", "costo": 12.50 },
"qty": 3,
"sort_order": 0
}
]Example — local items with create modal:
Downstage.orderList.mount('#invoice', {
name: 'invoice_items',
itemKey: 'codice',
options: [
{ value: { codice: 'P001', nome: 'Widget', unita: 'pz', costo: 12.5 }, label: 'P001 — Widget' },
],
columns: [
{ key: 'codice', label: 'Code', from: 'value.codice' },
{ key: 'nome', label: 'Product', from: 'value.nome' },
{ key: 'qty', label: 'Qty', type: 'qty' },
{ key: 'costo', label: 'Unit price', from: 'value.costo' },
{ key: 'totale', label: 'Total', compute: function (item) {
return ((item.qty || 1) * (item.value.costo || 0)).toFixed(2);
}
},
],
createFields: [
{ key: 'codice', label: 'Code', type: 'text', required: true },
{ key: 'nome', label: 'Name', type: 'text', required: true },
{ key: 'unita', label: 'Unit', type: 'select', options: ['pz','h','yr'] },
{ key: 'costo', label: 'Price', type: 'number', required: true, step: '0.01' },
],
createOption: function (data) {
return fetch('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(function (r) { return r.json(); });
},
});Example — remote fetch API:
Downstage.orderList.mount('#order', {
fetchUrl: '/api/products', // GET /api/products?q=widget
// OR: fetchOptions: function (q) { return fetch(…).then(…); },
itemKey: 'id',
columns: [
{ key: 'id', label: 'ID', from: 'value.id' },
{ key: 'name', label: 'Name', from: 'value.name' },
{ key: 'qty', label: 'Qty', type: 'qty' },
{ key: 'price', label: 'Price', from: 'value.price' },
{ key: 'total', label: 'Total', compute: function (item) {
return ((item.qty || 1) * (item.value.price || 0)).toFixed(2);
}
},
],
});HTML editor (detailed)
The editor uses document.execCommand on a contenteditable region (browser-dependent but widely supported). Link insertion uses a modal for URL type (mailto, tel, custom), target, and rel.
Mounting
Programmatic
Downstage.htmlEditor.mount("#editor", { preset: "demo", initialHtml: "<p>…</p>", showRawSwitch: false, toolbarExclude: [], iconsBase: "downstage-icons.svg", uid: "optional-stable-id", });Declarative — empty or content-filled container:
<div class="html-editor" data-html-editor-mount data-html-editor-preset="demo" data-html-editor-placeholder="…" data-html-editor-icons="downstage-icons.svg" data-html-editor-show-raw data-html-editor-exclude="bold,strikeThrough" > <p>Optional initial HTML here…</p> </div>data-html-editor-show-raw: attribute present ⇒ rich/HTML source switch on;="false"or="0"⇒ off.data-html-editor-exclude: comma- or space-separated command ids (see below).
Initial HTML resolution
- If
options.initialHtmlis passed tomount, it wins. - Else, inner HTML of the mount container (before it is cleared) is used.
- Else, preset default (e.g. demo paragraph).
Options (merged with preset)
| Option | Default (demo preset) | Description |
| ------ | ---------------------- | ----------- |
| preset | 'demo' | Key in Downstage.htmlEditor.presets. |
| initialHtml | preset | HTML string for the editable region. |
| placeholder | preset | data-placeholder on the contenteditable. |
| iconsBase | 'downstage-icons.svg' | Sprite path for modal and toolbar icons. |
| showRawSwitch | false | If true, shows Rich text / HTML source switch and raw <textarea>. |
| toolbarExclude | [] | List of toolbar command ids to omit (case-insensitive). |
| linkModal | preset object | Shallow merge with preset; nested linkModal fields merge for dialog copy. |
| uid | random | Suffix for dialog heading id (accessibility). |
Toolbar command ids (for toolbarExclude):bold, italic, underline, strikeThrough, createLink, justifyLeft, justifyCenter, justifyRight, justifyFull, h1–h7, p, inlineCode, blockquote.
“H7”: not a native element; implemented as <p class="ds-h7"> with role="heading" and aria-level="7".
Legacy markup
You can still hand-write the full editor DOM under [data-html-editor] (toolbar + content + link modal) and only call init() — no data-html-editor-mount. Do not set data-html-editor-mounted unless already initialized.
Documentation site
index.html— short home with links into the docs.docs/index.html— hub with cards to every topic (getting started, icons, layout & forms, apps & data, media, templates, etc.).- Topic pages — each file under
docs/focuses on one area; several use thedocs-layoutpattern (sidebar + chapters) for in-page navigation. catalog-full.html— optional single-page export of the entire gallery (useful for search-in-page or diffing). Regenerate topic pages withpython3 demo/split_catalog.pyafter editingcatalog-full.html.
Open pages via a static server when possible so SVG <use href> resolves correctly (some browsers restrict file:// external sprites). Demo assets: demo/demo.css, demo/demo.js, demo/kanban-board.json.
Active navigation (docs)
demo/demo.js runs on every page that includes it (home and all docs/*.html files):
Browse (navbar dropdown) — The link whose
hrefresolves to the same HTML filename as the current page getsclass="active"(matches.navbar-dropdown-link.activeindownstage.css). On the documentation hub (/docs,/docs/, ordocs/index.html), no topic row is marked active. Deep links such asdocs/getting-started.html#typographyonly affect the filename for this step; the hash is ignored here.On this page (sidebar) — On pages that use
docs-layoutwithnav.docs-navand anchor links (href="#…"), the script highlights the section currently in view (scroll position) and updates when the hash changes (e.g. after opening…#typography). The active item usesnav.docs-nav a.active(seedownstage.css). Matching sidebar links getaria-current="location"for assistive tech.
Philosophy
- CSS variables over utility soup — readable, themeable tokens.
- Semantic components —
.btn-primary, not dozens of atomic classes. - One theme surface — change variables, the whole UI shifts.
- JS with built-in components —
downstage.jsships ready-made modules; core layout and style need only CSS. - Mobile-first — sensible breakpoints without breakpoint explosion.
- Accessibility — focus styles, contrast, ARIA patterns where components require JS.
Customization
Load a file after downstage.css:
:root {
--brand-primary: #2c5f5d;
--radius: 2px;
--container: 800px;
}
[data-theme="dark"] {
--brand-primary: #7fbfa3;
}AI assistants
For Cursor, Copilot, and similar tools, the repo includes downstage-ai-guidelines.json: a small JSON document (schemaVersion, sections, examples, antiPatterns, cursorUsage) you can feed to assistants so they follow downstage conventions (tokens, Downstage API, doc locations).
Using it: copy the text into .cursor/rules, an AGENTS.md file, or your team’s prompt template; if you use npm install downstage, the file is node_modules/downstage/downstage-ai-guidelines.json. You can also fetch it from static hosting alongside your site. A plain HTTP URL is not an MCP server by itself—if you use MCP in Cursor, point a local MCP tool at that URL or at this file on disk.
Bump schemaVersion in the JSON when you change its shape for automation.
License
- CSS / JS / icons in this repo: MIT
- Space Grotesk (fonts): SIL Open Font License (OFL)
