npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

downstage

v1.0.2

Published

Minimal Nordic design system — CSS, zero-dependency JS components, icon sprite

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

  1. File structure
  2. npm
  3. Quick start
  4. Theme
  5. Design tokens (CSS variables)
  6. Typography & text utilities
  7. Icons
  8. Layout
  9. Components
  10. Utilities
  11. JavaScript (downstage.js)
  12. HTML editor (detailed)
  13. Documentation site
  14. Philosophy
  15. Customization
  16. AI assistants
  17. 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 gallery

npm

Published as downstage.

npm install downstage

After install, assets live under node_modules/downstage/:

| File / folder | Role | | ------------- | ---- | | downstage.css | Styles (package.jsonstyle) | | 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 h1h6 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: .field wrapping .label + .input / .textarea / .select
  • Password: <input type="password" class="input">downstage.js wraps it with an eye toggle (show / hide). Use data-password-toggle="off" to disable. Labels: password.show / password.hide in locales/*.json.
  • Copy: wrap a readonly textarea or disabled input in .input-copy-wrap with a button.input-copy-btn (optional span.input-copy-btn-label). Add .input-copy-wrap-minimal with textarea-minimal / input-minimal for underline-on-background styling. Strings copyField.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-list for filenames

Merges dropped files into the input; click on zone opens file dialog.

HTML editor

See HTML editor (detailed).

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). See docs/utilities.html.
  • Display / flex helpers: as in CSS (e.g. utility clusters in demos).
  • Reduced motion: prefers-reduced-motion respected 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

  1. Programmatic

    Downstage.htmlEditor.mount("#editor", {
      preset: "demo",
      initialHtml: "<p>…</p>",
      showRawSwitch: false,
      toolbarExclude: [],
      iconsBase: "downstage-icons.svg",
      uid: "optional-stable-id",
    });
  2. 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

  1. If options.initialHtml is passed to mount, it wins.
  2. Else, inner HTML of the mount container (before it is cleared) is used.
  3. 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, h1h7, 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 the docs-layout pattern (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 with python3 demo/split_catalog.py after editing catalog-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):

  1. Browse (navbar dropdown) — The link whose href resolves to the same HTML filename as the current page gets class="active" (matches .navbar-dropdown-link.active in downstage.css). On the documentation hub (/docs, /docs/, or docs/index.html), no topic row is marked active. Deep links such as docs/getting-started.html#typography only affect the filename for this step; the hash is ignored here.

  2. On this page (sidebar) — On pages that use docs-layout with nav.docs-nav and 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 uses nav.docs-nav a.active (see downstage.css). Matching sidebar links get aria-current="location" for assistive tech.


Philosophy

  1. CSS variables over utility soup — readable, themeable tokens.
  2. Semantic components.btn-primary, not dozens of atomic classes.
  3. One theme surface — change variables, the whole UI shifts.
  4. JS with built-in componentsdownstage.js ships ready-made modules; core layout and style need only CSS.
  5. Mobile-first — sensible breakpoints without breakpoint explosion.
  6. 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)