← Back to Typography

Typography pattern

Accessible data table

A clearly labeled comparison table with proper headers, aligned values, and safe narrow-screen scrolling.

Live example

See the pattern in isolation

The preview uses the same locked-down composition policy as the Agate IDE.

PreviewSandboxed

Canonical source

Copy it, change it, keep it plain

These exact files power the preview and the IDE handoff.

Source

HTML

<section class="comparison" aria-labelledby="comparison-title">
  <p class="comparison__eyebrow">Plan your visit</p>
  <h1 id="comparison-title">Trail conditions this week</h1>
  <p id="comparison-note">Distances are round trips. Conditions were last checked September 2.</p>

  <div class="table-scroll" role="region" aria-labelledby="comparison-title" tabindex="0">
    <table aria-describedby="comparison-note">
      <caption>Three available walking routes</caption>
      <thead>
        <tr>
          <th scope="col">Route</th>
          <th scope="col">Distance</th>
          <th scope="col">Climb</th>
          <th scope="col">Condition</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <th scope="row">River loop</th>
          <td class="number">2.4 mi</td>
          <td class="number">120 ft</td>
          <td>Dry and clear</td>
        </tr>
        <tr>
          <th scope="row">Cedar ridge</th>
          <td class="number">4.8 mi</td>
          <td class="number">760 ft</td>
          <td>Muddy near summit</td>
        </tr>
        <tr>
          <th scope="row">Lookout path</th>
          <td class="number">6.1 mi</td>
          <td class="number">1,180 ft</td>
          <td>Loose rock</td>
        </tr>
      </tbody>
    </table>
  </div>
</section>
Source

CSS

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  padding: clamp(1.5rem, 6vw, 5rem);
  background: #f7efe6;
  color: #2b151d;
  font-family: system-ui, sans-serif;
}

.comparison {
  width: min(100%, 58rem);
  margin-inline: auto;
}

.comparison__eyebrow {
  margin: 0 0 0.75rem;
  color: #8e2f50;
  font-size: 0.78rem;
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.comparison h1 {
  margin: 0;
  font-size: clamp(2.25rem, 7vw, 4.5rem);
  line-height: 1;
}

#comparison-note {
  max-width: 58ch;
  margin: 1rem 0 2rem;
  color: #5c3b45;
  line-height: 1.55;
}

.table-scroll {
  overflow-x: auto;
  border: 4px solid #2b151d;
  background: #fffaf5;
}

.table-scroll:focus-visible {
  outline: 4px solid #176b87;
  outline-offset: 4px;
}

table {
  width: 100%;
  min-width: 38rem;
  border-collapse: collapse;
  text-align: left;
}

caption {
  padding: 1rem;
  background: #b0436a;
  color: #ffffff;
  font-weight: 800;
  text-align: left;
}

th,
td {
  padding: 1rem;
  border-block-end: 1px solid #bca6ac;
}

thead th {
  background: #ead8ca;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

thead th:nth-child(2),
thead th:nth-child(3) {
  text-align: right;
}

tbody th {
  font-weight: 800;
}

tbody tr:nth-child(even) {
  background: #f3e6dc;
}

tbody tr:last-child > * {
  border-block-end: 0;
}

.number {
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

How it works

A small pattern with visible decisions

A caption names the table, column headers describe each kind of value, and row headers identify every route. Those relationships remain available to screen readers because the markup uses real table elements and explicit scope attributes.

The wrapper scrolls horizontally when the table needs more room. Giving that region a label and keyboard focus makes the overflow reachable without compressing the data into unreadable columns.

Make it yours

Open both files in the workspace.

Agate will protect different saved work before loading this example.