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.
--fluidfills the width;--narrowis 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,__bodyand__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.