02 — Components

Buttons

<div class="cluster">
  <button class="btn">Default</button>
  <button class="btn btn-primary">
    <svg class="icon"><use href="downstage-icons.svg#save" /></svg>
    Save
  </button>
  <button class="btn btn-secondary">
    <svg class="icon"><use href="downstage-icons.svg#filter" /></svg>
    Filter
  </button>
  <button class="btn btn-ghost">Ghost</button>
  <button class="btn btn-danger">
    <svg class="icon"><use href="downstage-icons.svg#trash" /></svg>
    Delete
  </button>
  <button class="btn btn-danger-solid">
    <svg class="icon"><use href="downstage-icons.svg#x" /></svg>
    Confirm
  </button>
  <button class="btn btn-success">
    <svg class="icon"><use href="downstage-icons.svg#check" /></svg>
    Approve
  </button>
  <button class="btn" disabled>Disabled</button>
</div>

Sizes

<button class="btn btn-sm btn-primary">Small</button>
<button class="btn btn-primary">Default</button>
<button class="btn btn-lg btn-primary">Large</button>

03 — Components

Forms

Create account

Card form with text inputs, select, checkbox, switch, and validation states.

Create account

Demo
We will never share your email.
At least 8 characters.
Auto-save enabled
<div class="card">
  <div class="card-header">
    <h4 style="margin: 0;">
      <svg class="icon"><use href="downstage-icons.svg#user-plus" /></svg>
      Create account
    </h4>
    <span class="badge">Demo</span>
  </div>

  <div class="stack">
    <div class="field">
      <label class="label" for="username">Username</label>
      <input id="username" class="input" type="text" placeholder="username">
    </div>

    <div class="field">
      <label class="label" for="email">Email</label>
      <input id="email" class="input" type="email" placeholder="you@example.com">
      <span class="help">We will never share your email.</span>
    </div>

    <div class="field has-error">
      <label class="label" for="password">Password</label>
      <input id="password" class="input" type="password">
      <span class="error">At least 8 characters.</span>
    </div>

    <div class="field">
      <label class="label" for="role">Role</label>
      <select id="role" class="select">
        <option>Developer</option>
        <option>Designer</option>
      </select>
    </div>

    <label class="check">
      <input type="checkbox" checked>
      <span>I accept the terms of service</span>
    </label>

    <div class="cluster" style="justify-content: space-between;">
      <span class="text-subtle">Auto-save enabled</span>
      <label class="switch">
        <input type="checkbox" checked>
        <span class="switch-slider"></span>
      </label>
    </div>
  </div>

  <div class="card-footer">
    <button class="btn btn-secondary">Cancel</button>
    <button class="btn btn-primary">
      <svg class="icon"><use href="downstage-icons.svg#save" /></svg>
      Save
    </button>
  </div>
</div>

Minimal underline

Single bottom border, transparent to the page background (--bg).

Attachment
Drop files here or PDF, PNG — demo only
<form class="stack">
  <div class="field">
    <label class="label" for="name">Name</label>
    <input id="name" class="input input-minimal" type="text" placeholder="Your name">
  </div>
  <div class="field">
    <label class="label" for="email">Email</label>
    <input id="email" class="input input-minimal" type="email" placeholder="you@example.com">
  </div>
  <div class="field">
    <label class="label" for="topic">Topic</label>
    <select id="topic" class="select select-minimal">
      <option>General</option>
      <option>Support</option>
    </select>
  </div>
  <div class="field">
    <label class="label" for="message">Message</label>
    <textarea id="message" class="textarea textarea-minimal" placeholder="Brief message"></textarea>
  </div>
  <div class="field">
    <span class="label">Attachment</span>
    <div class="upload-drop" data-upload-drop>
      <input class="sr-only" type="file" multiple>
      <div class="upload-drop-zone" tabindex="0" role="button">
        <svg class="icon icon-lg" aria-hidden="true">
          <use href="downstage-icons.svg#upload" />
        </svg>
        <span class="upload-drop-text">Drop files here or <label class="upload-drop-browse">browse</label></span>
        <span class="help">PDF, PNG</span>
      </div>
      <ul class="upload-drop-list" hidden></ul>
    </div>
  </div>
  <div class="cluster mt-2" style="justify-content: flex-end;">
    <button type="button" class="btn btn-ghost">Cancel</button>
    <button type="submit" class="btn btn-primary">Send</button>
  </div>
</form>

03a — Components

Input prefix & suffix

Wrap the control in .input-affix and add .input-affix__prefix and/or .input-affix__suffix for non-editable labels (scheme, currency, units). The inner field keeps .input; focus ring applies to the whole group via :focus-within.

Prefix is visual only — append the scheme in your submit handler if needed.
<div class="field">
  <label class="label" for="url">Website</label>
  <div class="input-affix">
    <span class="input-affix__prefix">https://</span>
    <input id="url" class="input" type="text" placeholder="example.com">
  </div>
</div>

<div class="field">
  <label class="label" for="amt">Amount</label>
  <div class="input-affix">
    <input id="amt" class="input" type="text" placeholder="0,00">
    <span class="input-affix__suffix">€</span>
  </div>
</div>

03b — Components

Form validation (JS)

downstage.js includes a small helper: add data-form-validate on a <form> (or call Downstage.formValidate.mount(form, options)). It checks required, type="email", minlength, and pattern, toggles .field.has-error, and fills a .error node when present. Messages come from i18n (formValidate.*) or from data-error-* on the .field.

Optional — six uppercase letters or digits if provided.
<form id="demo-form-validation" data-form-validate novalidate>
  <div class="field">
    <label class="label" for="name">Name</label>
    <input id="name" class="input" type="text" name="name" required minlength="2">
    <span class="error" aria-live="polite"></span>
  </div>
  <div class="field">
    <label class="label" for="email">Email</label>
    <input id="email" class="input" type="email" name="email" required>
    <span class="error" aria-live="polite"></span>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

Put each control in .field with a sibling .error for messages. Use novalidate so the script controls display.

// Auto: any form with data-form-validate on DOMContentLoaded

// Manual:
Downstage.formValidate.mount(document.querySelector('#my-form'), {
  live: true,
  onValid: function (e) { e.preventDefault(); /* fetch… */ }
});

// Field API (e.g. after dynamic value set):
Downstage.formValidate.validateField(document.querySelector('#email'));
OptionTypeDefaultDescription
live boolean true Re-validate on input/blur after an error was shown.
onValid function If the form passes validation on submit, called as onValid.call(form, event); use preventDefault to handle async submit.

Per-field message overrides on .field or input: data-error-required, data-error-email, data-error-minlength, data-error-pattern. Otherwise strings come from i18n formValidate.*.

Validated constraints: required (including checkboxes), type="email", minlength, pattern.

03c — Components

Date fields

Native date and datetime-local inputs, plus a date range pair with automatic min / max constraints between the two fields.

Date input

A single date field. Add data-datepicker-type="datetime" for date+time.

<!-- Date only -->
<div class="field">
  <label class="label" for="birthday">Date only</label>
  <div data-datepicker data-datepicker-name="birthday">
    <input id="birthday" class="input" type="date">
  </div>
</div>

<!-- Date + time -->
<div class="field">
  <label class="label" for="appt">Date + time</label>
  <div data-datepicker data-datepicker-type="datetime" data-datepicker-name="appointment">
    <input id="appt" class="input" type="datetime-local">
  </div>
</div>

Date range

Two linked fields — selecting a From date automatically sets min on the To field (and vice-versa). Works with both date and datetime.

Try picking a From date — the To field will prevent earlier dates.

<!-- Date range (date only) -->
<div data-daterange
  data-daterange-from-name="start_date"
  data-daterange-to-name="end_date"></div>

<!-- Datetime range -->
<div data-daterange
  data-daterange-type="datetime"
  data-daterange-from-name="start_at"
  data-daterange-to-name="end_at"></div>

<!-- With global min / max boundaries -->
<div data-daterange
  data-daterange-min="2026-01-01"
  data-daterange-max="2026-12-31"
  data-daterange-from-name="from"
  data-daterange-to-name="to"></div>
// Programmatic mount
var range = Downstage.dateRange.mount('#my-range', {
  type: 'datetime',       // 'date' (default) or 'datetime'
  fromName: 'start_at',
  toName: 'end_at',
  min: '2026-01-01',      // global floor
  max: '2026-12-31',      // global ceiling
  fromLabel: 'Check-in',  // custom labels
  toLabel: 'Check-out',
});

// API
range.getFrom();           // '2026-04-09'
range.getTo();             // '2026-04-12'
range.setFrom('2026-05-01');
range.setTo('2026-05-07');
range.setMin('2026-04-01');
range.setMax('2026-06-30');

// Event
el.addEventListener('daterange-change', function (e) {
  console.log(e.detail.from, e.detail.to);
});

Datetime range

Same component with data-daterange-type="datetime".

<div data-daterange
  data-daterange-type="datetime"
  data-daterange-from-name="start_at"
  data-daterange-to-name="end_at"></div>

04 — Components

Copyable fields

Read-only or disabled values with a copy action (.input-copy-wrap + downstage.js). Use .input-copy-wrap-minimal for underline controls on the page background.

<!-- Read-only textarea with copy -->
<div class="field">
  <label class="label">Read-only textarea</label>
  <div class="input-copy-wrap">
    <textarea class="textarea" readonly rows="4">Content here</textarea>
    <button type="button" class="input-copy-btn">
      <svg class="icon icon-sm" aria-hidden="true">
        <use href="downstage-icons.svg#copy" />
      </svg>
      <span class="input-copy-btn-label">Copy</span>
    </button>
  </div>
</div>

<!-- Disabled input with copy -->
<div class="field">
  <label class="label">Disabled input</label>
  <div class="input-copy-wrap">
    <input class="input" type="text" value="..." disabled>
    <button type="button" class="input-copy-btn">
      <svg class="icon icon-sm" aria-hidden="true">
        <use href="downstage-icons.svg#copy" />
      </svg>
      <span class="input-copy-btn-label">Copy</span>
    </button>
  </div>
</div>

<!-- Minimal textarea with copy -->
<div class="field">
  <label class="label">Minimal textarea</label>
  <div class="input-copy-wrap input-copy-wrap-minimal">
    <textarea class="textarea textarea-minimal" readonly rows="4">Content</textarea>
    <button type="button" class="input-copy-btn">
      <svg class="icon icon-sm" aria-hidden="true">
        <use href="downstage-icons.svg#copy" />
      </svg>
      <span class="input-copy-btn-label">Copy</span>
    </button>
  </div>
</div>

<!-- Minimal disabled input with copy -->
<div class="field">
  <label class="label">Minimal disabled input</label>
  <div class="input-copy-wrap input-copy-wrap-minimal">
    <input class="input input-minimal" type="text" value="..." disabled>
    <button type="button" class="input-copy-btn">
      <svg class="icon icon-sm" aria-hidden="true">
        <use href="downstage-icons.svg#copy" />
      </svg>
      <span class="input-copy-btn-label">Copy</span>
    </button>
  </div>
</div>

05 — Components

Alerts

Contextual feedback messages. Use .alert with a variant class (.alert-info, .alert-success, .alert-warning, .alert-danger).

Info — This is an informational alert.
Success — Changes have been saved.
Warning — Your session expires in 5 minutes.
Error — Unable to complete the operation.
<div class="alert alert-info">
  <svg class="icon icon-lg"><use href="downstage-icons.svg#info" /></svg>
  <div><strong>Info</strong> — Informational message.</div>
</div>

<div class="alert alert-success">
  <svg class="icon icon-lg"><use href="downstage-icons.svg#check-circle" /></svg>
  <div><strong>Success</strong> — Changes saved.</div>
</div>

<div class="alert alert-warning">
  <svg class="icon icon-lg"><use href="downstage-icons.svg#alert" /></svg>
  <div><strong>Warning</strong> — Session expires soon.</div>
</div>

<div class="alert alert-danger">
  <svg class="icon icon-lg"><use href="downstage-icons.svg#x-circle" /></svg>
  <div><strong>Error</strong> — Operation failed.</div>
</div>

06 — Components

Badges

Inline status labels. Use .badge with an optional variant class.

Default Success Danger Warning Info
<span class="badge">Default</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-danger">Danger</span>
<span class="badge badge-warning">Warning</span>
<span class="badge badge-info">Info</span>

07 — Components

File cards

Downloadable file links with icon, title, and metadata. Use a.file-card.

<a href="/files/document.pdf" class="file-card">
  <span class="file-card-icon">
    <svg class="icon icon-xl"><use href="downstage-icons.svg#file-pdf" /></svg>
  </span>
  <div class="file-card-body">
    <div class="file-card-title">document.pdf</div>
    <div class="file-card-meta">2.4 MB · 12 pages</div>
  </div>
  <svg class="icon"><use href="downstage-icons.svg#download" /></svg>
</a>

08 — Components

Table

Default in card, then minimal on --bg, compact, striped, and combined variants.

Project Status Owner Build
cipi.sh
v4.2.1
Stable team #1284
init
scaffolder
Beta team #0421
downstage.css
design system
RC team #0042
legacy-app
deprecated
EOL #9981
<div class="card" style="padding: 0; overflow: hidden;">
  <div class="table-wrap" role="region" aria-label="Table" tabindex="0">
    <table class="table">
      <thead>
        <tr>
          <th>Project</th>
          <th>Status</th>
          <th>Owner</th>
          <th class="text-right">Build</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>App</td>
          <td><span class="badge badge-success">Stable</span></td>
          <td>team</td>
          <td class="text-right text-mono">#1284</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

Minimal (page background)

No card: only bottom borders on rows, cells sit on --bg like minimal inputs. Class table table-minimal.

Project Status Owner Build
cipi.sh
v4.2.1
Stable team #1284
init
scaffolder
Beta team #0421
downstage.css
design system
RC team #0042
<div class="table-wrap" role="region" aria-label="Table" tabindex="0">
  <table class="table table-minimal">
    <thead>
      <tr>
        <th>Project</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>App</td>
        <td><span class="badge badge-success">Stable</span></td>
      </tr>
    </tbody>
  </table>
</div>

Compact

Tighter padding — table table-compact inside a card.

Key Value
theme light
version 0.4.0
license MIT
<div class="card" style="padding: 0; overflow: hidden;">
  <div class="table-wrap">
    <table class="table table-compact">
      <thead>
        <tr>
          <th>Key</th>
          <th>Value</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td class="text-mono">theme</td>
          <td>light</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

Striped

Alternating rows — table table-striped.

Event Time
Deploy 09:41
Build OK 09:38
PR merged 09:12
Push 09:10
<div class="card" style="padding: 0; overflow: hidden;">
  <div class="table-wrap">
    <table class="table table-striped">
      <thead>
        <tr>
          <th>Event</th>
          <th>Time</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Deploy</td>
          <td class="text-mono">09:41</td>
        </tr>
        <tr>
          <td>Build OK</td>
          <td class="text-mono">09:38</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

Minimal + compact

Combine modifiers: table table-minimal table-compact.

Setting Value
Font Space Grotesk
Radius 8px
Container 72ch
<div class="table-wrap" role="region" aria-label="Table" tabindex="0">
  <table class="table table-minimal table-compact">
    <thead>
      <tr>
        <th>Setting</th>
        <th class="text-right">Value</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Font</td>
        <td class="text-right text-mono">Space Grotesk</td>
      </tr>
    </tbody>
  </table>
</div>

09 — Components

HTML editor

Toolbar on contenteditable with document.execCommand and a link dialog (mailto, tel:, target, rel). Optional: rich text / HTML source switch via showRawSwitch; hide toolbar buttons with toolbarExclude. "H7" is <p class="ds-h7"> with role="heading" / aria-level="7". Initial HTML: inside data-html-editor-mount or initialHtml in JS.

Select text and use the toolbar to format.

<div class="html-editor" data-html-editor-mount>
  <p>Initial content goes here.</p>
</div>
Downstage.htmlEditor.mount(element, {
  showRawSwitch: true,
  toolbarExclude: ['h7'],
  initialHtml: '<p>Hello world</p>'
});

10 — Components

Timeline

Vertical ordered list with a line and markers. Use <ol class="timeline">; each entry is a .timeline-item with .timeline-marker and .timeline-content.

  1. First public release

    Single CSS, icon sprite, and JS with built-in components for tabs, accordion, and lightbox.

  2. Portfolio e blog

    Project cards and articles with a responsive grid and text variants.

    Release
  3. Timeline e team

    New blocks for timelines, profiles, and "link in bio" pages — still zero dependencies.

    Shipped
  4. Documentation and themes

    Variable guide, dark-theme examples, and copy-paste snippets for the most-used components.

    Planned
<ol class="timeline" aria-label="Roadmap">
  <li class="timeline-item">
    <span class="timeline-marker" aria-hidden="true"></span>
    <div class="timeline-content">
      <time class="timeline-date" datetime="2025-06">Jun 2025</time>
      <h3 class="timeline-title">Title</h3>
      <p class="timeline-desc">Description text.</p>
      <span class="timeline-badge"><span class="badge">Label</span></span>
    </div>
  </li>
</ol>

Compact variant

Add .timeline--compact for tighter line spacing.

  1. Accessibility audit

    Visible focus states and contrast checks on interactive components.

  2. Static examples

    Single demo page as a visual reference.

<ol class="timeline timeline--compact" aria-label="Milestones">
  <li class="timeline-item">
    <span class="timeline-marker" aria-hidden="true"></span>
    <div class="timeline-content">
      <time class="timeline-date" datetime="2026-04">Apr 2026</time>
      <h3 class="timeline-title">Title</h3>
      <p class="timeline-desc">Description text.</p>
    </div>
  </li>
</ol>

11 — Components

Mini map

A compact map for contact or venue blocks: same stack as Data UI → MapLeaflet + CARTO / OSM (no Google Maps). The marker uses a custom L.divIcon (CSS pin + brand color).

In a card

Title and address above the map, single bordered block.

Showroom

Duomo area · Milan

Minimal (unboxed)

Map strip only: no card chrome, no outer border; standard (smaller) pin.

<!-- In a card -->
<div class="ds-map-card">
  <div class="ds-map-card-body">
    <h3 class="ds-map-card-title">Venue</h3>
    <p class="ds-map-card-meta">Address line</p>
  </div>
  <div id="venue-map" class="ds-map-demo ds-map-demo--card"></div>
</div>

<!-- Minimal: map container only -->
<div id="venue-map-minimal" class="ds-map-demo ds-map-demo--minimal"></div>
var map = L.map('venue-map', {
  scrollWheelZoom: false,
  attributionControl: true
}).setView([45.4642, 9.19], 15);

L.tileLayer(
  'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
  { attribution: '…', subdomains: 'abcd', maxZoom: 20 }
).addTo(map);

var pin = L.divIcon({
  className: 'ds-leaflet-marker-wrap',
  html: '<div class="ds-leaflet-marker-pin ds-leaflet-marker-pin--card" ' +
    'style="--pin-color:var(--brand-primary)"></div>',
  iconSize: [40, 48],
  iconAnchor: [20, 48],
  popupAnchor: [0, -44]
});

L.marker([45.4642, 9.19], { icon: pin })
  .addTo(map)
  .bindPopup('We are here');