Skip to content

Components

Navigation

Navigation uses semantic lists of destinations, wraps on narrow screens, and can include native disclosure dropdowns or clearly separated actions.

Basic navigation

.menu is presentation on native navigation, not role="menu". Links keep normal Tab navigation; no JavaScript is required.

Use one or more lists inside a labelled nav. A label distinguishes this landmark from other navigation regions; aria-current="page" identifies and emphasizes the current destination.

Add .menu to opt into indicators for nav.menu > ul > li > a. These top-level plain links inherit color and weight, preserve explicit .primary, .secondary, and .contrast colors, have no text underline, and use inline-block layout with 0.35rem 0.5rem padding. A transparent --tty-frame-width bottom border reserves space: hover shows a 45% current-color border, while aria-current="page" keeps a solid border, including on hover. Keyboard focus outlines are unchanged.

Without .menu, navigation retains its horizontal layout and ordinary link treatment. Button-styled links (a.button and legacy a[role="button"]) and dropdown items keep their own styling. The opt-in does not depend on ancestors: leave it off sidebars and breadcrumbs.

<nav class="menu" aria-label="Console navigation">
  <ul>
    <li><a href="#basic"><strong>Console</strong></a></li>
  </ul>
  <ul>
    <li><a href="#basic" aria-current="page">Overview</a></li>
    <li><a href="#breadcrumbs">Queries</a></li>
    <li><a href="#actions">Settings</a></li>
  </ul>
</nav>

Pagination

Add .pagination to a labelled nav for links that share the framework’s secondary button styling, including borders, offset shadows, hover, and pressed states, and wrap within their container. No .button class is needed on individual links. Use direct links or a direct ul or ol; numbered pages are optional. Do not add .menu or button roles. URLs, cursor values, page counts, and available destinations belong to your application; tty.css supplies styling only.

Numbered pages

Use aria-current="page" on the current page link or a non-interactive span. The current page uses the primary button fill and pressed appearance, with an underline that identifies it without color. Unavailable spans share the disabled button treatment; ellipses and result summaries stay plain text. Give numeric links descriptive accessible names.

<nav class="pagination" aria-label="Search result pages">
  <ul>
    <li><a href="?page=1" rel="prev">Previous</a></li>
    <li><a href="?page=1" aria-label="Page 1">1</a></li>
    <li><a href="?page=2" aria-label="Page 2" aria-current="page">2</a></li>
    <li><a href="?page=3" aria-label="Page 3">3</a></li>
    <li><span aria-label="More pages">…</span></li>
    <li><a href="?page=20" aria-label="Page 20">20</a></li>
    <li><a href="?page=3" rel="next">Next</a></li>
  </ul>
  <small>Showing 21–40 of 400</small>
</nav>

Cursor-based pagination

Previous and next links work without page numbers, a current-page marker, or a known total. Use the opaque cursors returned by your application in real URLs. The optional result summary is ordinary text, not an automatic live announcement.

<nav class="pagination" aria-label="Event batches">
  <a href="?before=evt_021" rel="prev">Previous</a>
  <a href="?after=evt_040" rel="next">Next</a>
  <small>Showing 20 events</small>
</nav>

Boundaries and sequential content

Omit an unavailable destination, or replace its link with <span aria-disabled="true">. A span has no link behavior and is not in the Tab order. Do not use disabled on links, fake URLs, or aria-disabled alone on a working link. Sequential content can use descriptive labels instead of page numbers.

<nav class="pagination" aria-label="Post archives">
  <span aria-disabled="true">Newer posts</span>
  <a href="?before=post_040" rel="next">Older posts →</a>
</nav>

These previews demonstrate markup only; their query strings do not change the sample results. Real links retain normal Tab and Enter behavior, bookmarking, and opening in another tab. Icon-only links need an accessible name. If your application replaces results without navigation, it also owns focus management and result announcements.

Load more

Appending results is an action, not a destination. Use a regular button outside a pagination navigation landmark, and supply application code to fetch and append results, manage busy and unavailable states, and announce updates. No pagination-specific class is needed.

<button type="button">Load more results</button>

Checkbox dropdown

Menu rows may contain labelled checkboxes for multiple independent choices. Give each control a useful name; clicking a checkbox does not need to close the native disclosure.

<details class="dropdown">
  <summary>Select services</summary>
  <ul>
    <li><label><input type="checkbox" name="services" value="search" checked> Search</label></li>
    <li><label><input type="checkbox" name="services" value="storage"> Storage</label></li>
    <li><label><input type="checkbox" name="services" value="events"> Events</label></li>
  </ul>
</details>

Button dropdown and alignment

Direct buttons are styled as full-width action rows. Add .align-right to the dropdown to physically anchor the overlay to the right.

<details class="dropdown align-right">
  <summary>Workspace actions</summary>
  <ul>
    <li><button type="button">Create workspace</button></li>
    <li><button type="button">Import settings</button></li>
    <li><button type="button">Sign out</button></li>
  </ul>
</details>

Validation states

Put aria-invalid="true" or aria-invalid="false" on the summary for invalid and explicitly valid borders. Connect explanatory text with aria-describedby; there is no aria-valid attribute.

Region is available.
A deployment region is required.
<div class="grid">
  <div>
    <details class="dropdown">
      <summary aria-invalid="false" aria-describedby="region-valid">Region: US East</summary>
      <ul>
        <li><a href="#validation">US East</a></li>
        <li><a href="#validation">EU Central</a></li>
      </ul>
    </details>
    <small class="helper" id="region-valid">Region is available.</small>
  </div>
  <div>
    <details class="dropdown">
      <summary aria-invalid="true" aria-describedby="region-error">Choose a region</summary>
      <ul>
        <li><a href="#validation">US East</a></li>
        <li><a href="#validation">EU Central</a></li>
      </ul>
    </details>
    <small class="helper" id="region-error">A deployment region is required.</small>
  </div>
</div>

Navigation actions

Buttons may appear in navigation when they perform an action. Destinations remain links, and every button should declare its type.

<nav class="menu" aria-label="Deployment navigation">
  <ul>
    <li><a href="#actions" aria-current="page">Deployments</a></li>
    <li><a href="#actions">Logs</a></li>
  </ul>
  <ul>
    <li><button type="button" class="secondary">Refresh</button></li>
    <li><button type="button">New deployment</button></li>
  </ul>
</nav>

Native and application behavior

The browser makes details and summary operable by keyboard and pointer, exposes expanded state, and toggles the disclosure without JavaScript. These are disclosure lists, not ARIA menus, so do not add role="menu" or menu keyboard behavior to otherwise native markup.

tty.css only provides presentation. Application code is responsible for updating a choice summary, closing after selection when appropriate, closing on outside interaction or Escape if required, running button actions, and keeping validation messages and state synchronized. Preserve native link navigation and checkbox behavior when enhancing a dropdown.