Skip to content

Components

Native components

tty.css styles browser primitives and accessible states. HTML provides the behavior except where a dialog needs application code.

Details and disclosure

details and summary provide disclosure semantics, keyboard operation, and an exposed open state without JavaScript. Ordinary disclosures keep their default frame. Add .plain to details:not(.dropdown) to opt into a borderless, transparent surface, including inside fieldsets, while retaining contextual padding and the chevron; dropdowns are unaffected.

Why use a native disclosure?

It works before application JavaScript loads.

Can it contain rich content?

Yes. Add paragraphs, lists, forms, or other appropriate content.

Need a quieter disclosure?

Opt in with .plain; padding and the chevron remain.

<details open>
  <summary>Why use a native disclosure?</summary>
  <p>It works before application JavaScript loads.</p>
</details>
<details>
  <summary>Can it contain rich content?</summary>
  <p>Yes. Add paragraphs, lists, forms, or other appropriate content.</p>
</details>
<details class="plain" open>
  <summary>Need a quieter disclosure?</summary>
  <p>Opt in with .plain; padding and the chevron remain.</p>
</details>

Fieldset variant

Direct-child disclosures inside a fieldset become divided rows. Use this for related settings or questions; see the base fieldset guidance.

Deployment options
Build settings
Release policy

Deploy only after all required checks pass.

<fieldset>
  <legend>Deployment options</legend>
  <details>
    <summary>Build settings</summary>
    <label><input type="checkbox" name="minify"> Minify assets</label>
  </details>
  <details>
    <summary>Release policy</summary>
    <p>Deploy only after all required checks pass.</p>
  </details>
</fieldset>

Icons

tty.css does not include an icon library. For a matching pixel style, try Pixel Icon Library by HackerNoon. Follow their usage guide and license requirements.

Timeline has its own Timelines guide.

Busy and loading states

aria-busy="true" exposes a busy region or control and adds the tty.css spinner to non-form elements. Primary, secondary, contrast, and outline buttons keep their variant styling.

Indexing documents

<p aria-busy="true">Indexing documents</p>
<p class="group">
  <button type="button" aria-busy="true" disabled>Deploying</button>
  <button type="button" class="secondary" aria-busy="true" disabled>Synchronizing</button>
  <button type="button" class="contrast" aria-busy="true" disabled>Scanning</button>
  <button type="button" class="outline" aria-busy="true" disabled>Refreshing</button>
</p>
  • Set the attribute only while work is pending, then remove it or set it to false.
  • aria-busy does not prevent activation. Use native disabled while a form control must be unavailable, then remove both attributes when the operation completes.
  • A busy state does not guarantee that status text is announced. Put changing status in a suitable live region when users need asynchronous updates.
  • The spinner is generated content and collapses to one frame under reduced motion. Keep a meaningful text label.

Progress and meter

Use progress for task completion. Include value for determinate work and omit it while the amount is unknown. Use meter for a scalar measurement within a known range, not for task progress.

68%Scanning62%
<label for="build-progress">Build progress</label>
<progress id="build-progress" value="68" max="100">68%</progress>

<label for="scan-progress">Scanning repository</label>
<progress id="scan-progress">Scanning</progress>

<label for="cache-meter">Cache utilization</label>
<meter id="cache-meter" min="0" max="100" low="35" high="80" optimum="60" value="62">62%</meter>

Textured progress

Add class="textured" to a native progress with value for a continuous Dense SVG-mask fill over a muted --tty-border track. The bar is 1rem high with no outline, frame, or box shadow. Its fill uses currentColor; customize it with CSS color, not a role.

By default, the fill follows text color: black in the default light theme, light in the dark theme. The accent-colored example below explicitly overrides color.

68%68%
<label for="textured-progress-default">Default — inherited text color</label>
<progress class="textured" id="textured-progress-default" value="68" max="100">68%</progress>

<label for="textured-progress">Custom — theme accent color</label>
<progress class="textured" id="textured-progress" value="68" max="100"
  style="color: var(--tty-accent)">68%</progress>

Optional progress pattern tokens change the mask and tile size independently of frame shadows. Without value, progress.textured keeps the existing indeterminate style. Ordinary progress and meters are unchanged, and no framework JavaScript is needed. In forced-colors mode, the native fallback uses no mask and a Highlight fill.

Fallback text is useful where the elements are unsupported but is not a visible numeric label in modern browsers. Show a separate text value when the exact number matters.

Tooltips

data-tooltip supplies the tooltip text. Add data-placement="bottom", left, or right to change its position; top is the default. Tooltips appear on hover and keyboard focus, and left and right placements fall back above the trigger on narrow screens.

<p class="group">
  <button type="button" data-tooltip="Runs the current deployment">Top</button>
  <button type="button" class="secondary" data-tooltip="Shown below the control" data-placement="bottom">Bottom</button>
  <button type="button" class="outline" data-tooltip="Shown to the left" data-placement="left">Left</button>
  <button type="button" class="contrast" data-tooltip="Shown to the right" data-placement="right">Right</button>
</p>

Tooltip overlays are shadowless and do not support frame treatments.

The visual bubble is CSS generated content. It has no tooltip role, cannot be dismissed independently, is unavailable to touch-only interaction, and is not a reliable accessible description. Never put essential information in it. Keep the control's accessible name complete, and connect necessary help text with aria-describedby.

Starts deployment immediately and cannot be paused.
<button type="button" aria-describedby="deploy-help" data-tooltip="Starts deployment">Deploy</button>
<span class="sr-only" id="deploy-help">Starts deployment immediately and cannot be paused.</span>

Dialogs

tty.css presents native dialog elements but does not open or close them. Use showModal(), not the open attribute, for a modal: the browser then manages the top layer, backdrop, initial focus behavior, and Esc dismissal.

Plain dialog

A plain dialog is appropriate for a short message or compact form. A form with method="dialog" closes it without a custom close handler.

Session expired

Sign in again to continue.

<button type="button" data-dialog-open="plain-dialog">Open plain dialog</button>
<dialog id="plain-dialog" aria-labelledby="plain-dialog-title">
  <h3 id="plain-dialog-title">Session expired</h3>
  <p>Sign in again to continue.</p>
  <form method="dialog">
    <button type="submit">Close</button>
  </form>
</dialog>

Article dialog

A direct child article enables the wider structured layout with a sticky header, scrolling body, and sticky action footer. The dialog wrapper displays the default Standard SVG-mask shadow outside the scrolling article. Put frame treatments such as .strong-shadow or .flat on the dialog, not the article. The icon-only close button requires an accessible name.

Delete namespace?

The namespace search-production and all stored documents will be permanently removed.

This operation cannot be undone. Export anything you need before continuing.

<button type="button" data-dialog-open="article-dialog">Open article dialog</button>
<dialog id="article-dialog" aria-labelledby="article-dialog-title" aria-describedby="article-dialog-description">
  <article>
    <header>
      <button type="button" rel="prev" aria-label="Close dialog" data-dialog-close></button>
      <h3 id="article-dialog-title">Delete namespace?</h3>
    </header>
    <p id="article-dialog-description">The namespace <code>search-production</code> and all stored documents will be permanently removed.</p>
    <p>This operation cannot be undone. Export anything you need before continuing.</p>
    <footer>
      <button type="button" class="secondary" data-dialog-close>Cancel</button>
      <button type="button" data-dialog-close>Confirm deletion</button>
    </footer>
  </article>
</dialog>

Application-owned behavior

The application must bind controls, choose initial focus when browser defaults are unsuitable, return focus as appropriate, and complete or cancel the action. Add .modal-is-open to the root while a modal is open to prevent background scrolling; that class does not create modality.

const root = document.documentElement;

document.querySelectorAll("[data-dialog-open]").forEach((button) => {
  button.addEventListener("click", () => {
    const dialog = document.getElementById(button.dataset.dialogOpen);
    if (!dialog) return;

    root.classList.add("modal-is-open");
    dialog.showModal();
  });
});

document.querySelectorAll("[data-dialog-close]").forEach((button) => {
  button.addEventListener("click", () => button.closest("dialog")?.close());
});

document.querySelectorAll("dialog").forEach((dialog) => {
  dialog.addEventListener("close", () => {
    if (!document.querySelector("dialog[open]")) root.classList.remove("modal-is-open");
  });
});