Skip to content

Your first page

A page is a shell plus content. This walks through both, using only classes from the reference.

The smallest useful page

<div class="ads-container ads-stack">
  <div class="ads-page-header">
    <h1 class="ads-page-title">Contacts</h1>
    <p class="ads-muted">Everyone your team has spoken to.</p>
  </div>

  <div class="ads-card">
    <div class="ads-card__head">
      <h2 class="ads-card__title">Recent</h2>
    </div>
    <div class="ads-card__body ads-stack">
      <p>Nothing yet.</p>
    </div>
  </div>
</div>

Three ideas are doing the work:

ads-container
Centres content and applies the page gutter. --fluid fills the width; --narrow is for reading-width documents.
ads-stack
A vertical rhythm primitive: children are spaced by one gap, no per-element margins. See Layout primitives.
ads-card
A surface with an optional __head, __body and __foot. Cards are the default container for a block of related content — but not everything is a card.

Adding a table

Tables are the most common thing a business application gets wrong, so the linter has a rule about them:

<div class="ads-table-scroll">          <!-- required -->
  <table class="ads-table ads-table--vcenter">
    <thead>
      <tr>
        <th><button class="ads-th-sort" aria-sort="ascending">Name</button></th>
        <th>Company</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Amélie Rousseau</td>
        <td class="ads-muted">Northwind</td>
      </tr>
    </tbody>
  </table>
</div>

The wrapper is not optional

Without .ads-table-scroll, a table wider than its column scrolls the page sideways instead of itself, and the far columns become unreachable at small widths and at 200% text. abilian-ds lint reports this as table-overflow.

Adding form fields

Reach for the macros rather than hand-rolling — they carry the accessible wiring:

{% from "ads/form.html" import field, select, checkbox, button %}

<form method="post" class="ads-stack">
  {{ field("email", label="Email", type="email", required=true,
           help="We only use this for sign-in.",
           errors=form.errors.email) }}

  {{ select("role", label="Role", value=user.role,
            options=[("admin", "Administrator"), ("editor", "Editor")]) }}

  {{ checkbox("digest", label="Send me the weekly digest") }}

  {{ button("Save", type="submit") }}
</form>

A hand-written <input> will be missing its aria-describedby; field() will not. See Forms.

The application shell

For a page inside an application — sidebar, top bar, content — use the shell:

<div class="ads-app">
  <aside class="ads-sidebar">
    <h1 class="ads-brand"><a href="/">Acme</a></h1>
    <nav>
      <ul class="ads-nav__list">
        <li>
          <a class="ads-nav__link" aria-current="page" href="/contacts">
            <span class="ads-nav__icon">{{ icon("users") }}</span>
            <span>Contacts</span>
          </a>
        </li>
      </ul>
    </nav>
  </aside>

  <div class="ads-main">
    <header class="ads-topbar"></header>
    <main class="ads-page-body">
      <!-- the page above goes here -->
    </main>
  </div>
</div>

aria-current, not a class

The current nav item is marked with aria-current="page", and the stylesheet keys its highlight off that attribute. Writing class="is-active" gets you nothing — deliberately. The look cannot exist without the meaning, so it cannot be forgotten. Same for aria-pressed on a toggle and aria-invalid on a field.

Four shell layouts ship: vertical (default), folded, right-hand sidebar, and a horizontal top bar. See Application shells.

Next