A documentation space is a set of books. The top level is a shelf of covers; opening one shows its contents; opening a line in the contents lands you on a page. The book family gives you the first two of those, as four classes: , , , and .
The row in a contents list is a contents item, not a page — in a book, the thing you click in the contents is an entry and the thing you land on is the page. Naming them apart is what keeps the model legible.
Two components complete the model and live in their own docs: moving sideways between pages is Page Navigation; moving back up is Breadcrumb.
Anatomy
.book-shelf ← L0 grid of covers
.book-shelf-title ← optional group heading
a.book-cover ← one closed book
.book-cover-header
.book-cover-leading ← icon, varies per card
.book-cover-trailing ← icon, hints at what clicking does
.book-cover-body
.book-cover-title
.book-cover-description
.book-cover-footer
.book-cover-byline ← "by X"
.book-cover-meta ← metadata
.book-contents ← L1 table of contents
.book-contents-title ← optional group heading
a.book-contents-item ← one line in the contents
.book-contents-item-leading ← icon, varies per row
.book-contents-item-body
.book-contents-item-title
.book-contents-item-description
.book-contents-item-meta ← metadata, under the description
.book-contents-item-byline ← "by X", inside meta
.book-contents-item-trailing ← icon, hints at what clicking does
details.book-contents-item ← the same row, opening in place
summary ← the visible row: title + trailing
.book-contents-item-panel ← revealed: description + meta
Slots are named on two different principles, because they hold two different kinds of thing:
leading/trailingname a position. They frame the surface and hold an icon. Deliberately generic — another product's trailing icon might be a chevron, an arrow, or a download glyph, and the name should not argue with any of them.byline/metaname a content type. They carry text about the item, so naming them by position would say nothing useful about what belongs there.
On a cover, leading and trailing are the header's two ends; the footer's two ends are byline and meta, because what belongs there is text rather than icons. A row has only one line, so leading and trailing are unambiguous, and meta moves inside the body.
Neither icon slot may hold an interactive control. The card is already a link, so a nested button is invalid HTML and unreachable in several screen-reader modes. trailing holds an icon that hints at what clicking does — it is not an action control. A surface that needs a real second action wants a different component.
Every slot is optional. Empty ones are simply left out — the end slot's auto margin holds its side either way, so no spacers are needed.
Engine and instance. The component guarantees the slots. Which icon goes in trailing, and whether it stays the same down a shelf, is an instance decision. By Default keeps it constant, because constancy is what signals "this opens" — but that is a usage rule for this brand, not a property of the component. Another product can decide otherwise without the class names lying.
Note the row has no header or footer strip. A cover is a vertical surface with room for two strips; a row is one horizontal line, so leading starts it and trailing ends it. The cover's separate byline and meta collapse into meta on the row for the same reason.
Row meta sits inside the body, under the description — not as a fourth item on the line. On a cover, meta lives in the footer strip and gets a line of its own; a row has no strip, and leading and trailing both refuse to shrink, so a trailing meta would take its width straight out of the title. Under the description it wraps instead of squeezing.
Basic usage
A shelf holds covers. owns its own grid, so no utility classes are needed alongside it.
<div class="book-shelf">
<a href="/brand/" class="book-cover">
<header class="book-cover-header">
<div class="book-cover-trailing">
<div class="svg-icn" data-icon="open-full"><!-- inline svg --></div>
</div>
</header>
<div class="book-cover-body">
<h3 class="book-cover-title">Brand Book</h3>
<p class="book-cover-description">Brand identity, values, positioning, and visual guidelines</p>
</div>
<footer class="book-cover-footer">
<span class="book-cover-byline"><em>by</em> Studio</span>
</footer>
</a>
<!-- repeat for each book -->
</div>
Covers share a minimum height so a row of mixed-length titles still reads as a shelf. Resize every cover at once by re-pointing --book-cover-min-height; never override the height on a single card.
For auto-generated covers, the doc generator builds these from cms/*.md frontmatter. Set the byline with the author: frontmatter field, which falls back to Studio when omitted.
Using the header slots
The header is a strip with a start slot and an end slot. holds the open affordance on the right; is the optional left slot that says what this is.
Use leading when a shelf mixes kinds of thing and the title alone doesn't distinguish them — a brand book beside a tool beside a reference. On a shelf of like-for-like sections it is noise, so leave it out.
<header class="book-cover-header">
<div class="book-cover-leading">
<div class="svg-icn" data-icon="catalog"><!-- inline svg --></div>
</div>
<div class="book-cover-trailing">
<div class="svg-icn" data-icon="open-full"><!-- inline svg --></div>
</div>
</header>
The leading icon is visible at rest; the trailing icon fades in on hover and focus. That difference is the point — one says what this is, the other says what happens if you click.
A leading icon that only repeats the title needs aria-hidden="true". One that carries information the title doesn't needs an accessible name.
Using the footer slots
Same shape at the bottom: on the left, on the right. Meta is a status slot — a count, a date, an access level — and Tag or Badge sit in it directly.
<footer class="book-cover-footer">
<span class="book-cover-byline"><em>by</em> Studio</span>
<span class="book-cover-meta">
<span class="tag">38 pages</span>
</span>
</footer>
Keep meta to a short noun. The whole card is one link, so its accessible name is every word inside it — a sentence in the footer makes each cover announce as a paragraph.
A colour-only status fails WCAG 1.4.1. "Team only" in an amber tag is fine; an amber dot on its own is not.
Sizes
data-size re-points height, title size, and content inset together. A short cover with a full-size title reads as a mistake rather than a variant, so the three move as one.
<a href="#" class="book-cover" data-size="small">…</a>
<a href="#" class="book-cover">…</a> <!-- default -->
<a href="#" class="book-cover" data-size="large">…</a>
| Size | Min height | Title | Title ≤959px | Content inset |
|---|---|---|---|---|
small |
170px |
|||
| default | 250px |
|||
large |
340px |
Each title drops two rungs below 960px, the same step the heading scale takes.
Keep one size per shelf. Uniform height is what makes an index read as a shelf rather than a pile — mixing sizes in one grid is the fastest way to lose that. The demo above breaks the rule only to show the three side by side.
Contents
Inside a book, entries stack as wide horizontal rows rather than tall centred cards. The wrapper draws the outer border; each item draws its own bottom rule.
<div class="book-contents">
<a href="/website/color.html" class="book-contents-item">
<div class="book-contents-item-body">
<h3 class="book-contents-item-title">Colour</h3>
<p class="book-contents-item-description" data-text-wrap="pretty">Palette, scales, and semantic colour mappings</p>
</div>
<div class="book-contents-item-trailing">
<div class="svg-icn" data-icon="open-full"><!-- inline svg --></div>
</div>
</a>
<!-- repeat for each entry -->
</div>
Using the row slots
A row carries the same slot vocabulary as a cover, but no header or footer strip. A cover is a vertical surface, so it has room for two strips with two ends each. A row is one horizontal line: leading at the start, trailing at the end, and everything else in the body between them.
That is also why the cover's separate byline and meta slots collapse here — on a footer strip they are two ends with two jobs, but a row has no strip, so the byline sits inside meta. Meta takes whatever the row needs to say about itself: an access tag, a byline, a date, or several.
<a href="/brand/rate-card.html" class="book-contents-item">
<div class="book-contents-item-leading">
<div class="svg-icn" data-icon="lock"><!-- inline svg --></div>
</div>
<div class="book-contents-item-body">
<h3 class="book-contents-item-title">Rate Card</h3>
<p class="book-contents-item-description" data-text-wrap="pretty">Commercial terms and partner pricing</p>
<span class="book-contents-item-meta">
<span class="tag" data-color="warning">Team only</span>
</span>
</div>
<div class="book-contents-item-trailing">
<div class="svg-icn" data-icon="open-full"><!-- inline svg --></div>
</div>
</a>
Both slots are optional and independent — the third row above has meta but no identity, and mixing them down a list is fine.
Meta wraps rather than competing, so a row tolerates more here than a cover footer does. Identity and action still refuse to shrink, so keep those to a single icon.
Columns
data-cols re-points the column token. Two columns is the default and the canonical index layout — it gives every book room to breathe. Three suits shorter titles and denser sets.
<div class="book-shelf" data-cols="3">…</div>
Both settings collapse to a single column at 768px. The breakpoint belongs to the component rather than the grid utility, so it travels with it into another product.
Group headings
and label a group within a longer index. They carry the eyebrow treatment without depending on the utility, so a product can restyle them on their own.
<h2 class="book-contents-title">Foundations</h2>
<div class="book-contents">…</div>
Contents as disclosures
A contents row navigates. When the row's content is short enough to live in place — a glossary definition, a spec, a one-paragraph answer — opening beats navigating, and the row becomes a <details> wearing the same class. The summary is the row; the panel is what opening reveals.
The title element depends on what the list is. A row title is a <span> when the list is navigation — a contents index the reader scans to choose a destination, where flooding the outline with row titles helps nobody. It becomes an <h3> when the list is the page's content: every row carries an anchor id, the rows are the thing the reader came for, and the page type emits no TOC. The glossary is the reference case. overrides margin, size, line-height and weight either way; the heading's slightly tighter letter-spacing is the one visible difference, and it matches the anchor rows, which have always used <h3>.
Panels with more than one paragraph may section themselves with <h4 class="label"> subtitles — real sub-sections only, in entries long enough to need them. The closing examples line is not one of those sections: it is a fixed field, so its label is a span inside the meta slot, not a heading.
<span class="book-contents-item-meta"><span class="label">Examples</span> the newsletter · a Reel</span>
Channel
A channel is any place where the brand meets an audience directly.
Format
A format is the unit a channel accepts; regulations are scoped here.
<div class="book-contents">
<details class="book-contents-item" name="glossary-term" id="term-channel">
<summary>
<h3 class="book-contents-item-title">Channel</h3>
<div class="book-contents-item-trailing">
<div class="svg-icn" data-icon="chevron-down"><!-- inline svg --></div>
</div>
</summary>
<div class="book-contents-item-panel">
<p class="book-contents-item-description">A channel is any place where the brand meets an audience directly.</p>
<span class="book-contents-item-meta"><span class="label">Examples</span> the newsletter · the Instagram account</span>
</div>
</details>
</div>
The code sample shows the reference-list form: <h3> title, anchor id, labelled examples. On a navigation list the title is a <span> and the id is optional — see the title rule above.
Single-open or multi-open. The same name value across a list gives native single-open — opening one row closes the rest, no JavaScript. Omit name and each row toggles independently. Older browsers without name support fall back to multi-open, which degrades gracefully. Never author open on more than one row in a name group — it is invalid, and browsers resolve it by closing all but one unpredictably.
The trailing icon is visible at rest, unlike the anchor row's hover-revealed arrow: a constant chevron says "opens in place" where the appearing arrow says "navigates". It rotates on open; under reduced motion the rotation is instant — the state change itself is the feedback.
No ARIA is needed. <details>/<summary> announce the disclosure role and the expanded state natively; adding aria-expanded double-announces in some screen readers. The chevron is decorative and already carries aria-hidden through the wrapper. Interactive content remains forbidden in every slot — the summary is already the control.
Accessibility
- Use
<a>, never a<div>with a click handler. Every cover and contents item that navigates is a link; the whole card is the link, so the accessible name is its full text content — title, description, and author together. A row that opens in place is a<details>instead — see "Contents as disclosures" — never a scripted div either way. - Keep
metashort on link rows. Anything in or inside an<a>joins the link's accessible name. A date or a one-word status is fine; a sentence makes every card announce as a paragraph. The ceiling does not apply to a disclosure row — there the meta sits in the revealed panel and joins no accessible name. - The trailing icon is decorative and must carry
aria-hidden="true". It repeats what the link already says. Icons rendered through the design system's wrapper already do this. - A leading icon needs a decision. If it duplicates the title, hide it with
aria-hidden="true"; if it carries information the title does not, give it an accessible name. - Never nest interactive content in a slot. The card is already a link — a button or second link inside it is invalid HTML and unreachable in several screen-reader modes. A card needing a second action should be restructured so the action is a sibling of the link, not a child.
- Heading levels are the caller's responsibility. Cards render
<h3>. Place a group heading (<h2>) above the shelf or contents list so the page does not jump from<h1>to<h3>. - Focus and hover reveal the trailing icon equally. A keyboard user tabbing a shelf sees the same affordance a mouse user does. Both states also draw the focus ring, which is the real affordance — do not remove it.
- Reduced motion is handled.
--book-cover-liftis re-pointed to0underprefers-reduced-motion: reduce, so the hover lift stops while colour feedback stays. - Status must not be colour-only. A badge in a meta slot needs text or an icon alongside the colour.
Usage rules
Do:
- Use + on overview and landing pages, wherever a visitor chooses a section to open
- Use + one level down, listing the pages inside a section
- Keep cover titles to one to four words so they do not wrap to a third line
- Keep descriptions to one sentence; covers already set
text-wrap: pretty, contents rows needdata-text-wrap="pretty"explicitly - Re-point tokens to restyle; a product changes
--book-cover-min-height, not the rule
Don't:
- Don't add alongside — it is standalone and ships its own background, border, and hover states
- Don't override
min-heighton a single cover; uniform height is what makes a shelf read as a shelf - Don't swap the trailing icon per card — constancy is what signals "this opens". That is By Default's usage rule, not a component constraint
- Don't put extra content in ; the centred slot is title and description only
- Don't mix and in one grid — pick one pattern per surface
- Don't wrap a shelf in
.grid.cols-2.gap-xl; the shelf owns its layout - Don't mix sizes in one shelf — uniform height is what makes an index read as a shelf
- Don't give a contents row a header or footer strip; that turns a row into a card and blurs the shelf/contents distinction
CSS reference
This section documents how the component is built. For usage, see the sections above.
Tokens
| Token | Default | What it controls |
|---|---|---|
2 |
Column count on the shelf | |
| Space between covers | ||
250px |
Cover height — the shelf's visual rhythm | |
Cover title; data-size and the 960px step re-point it |
||
| Cover fill; dark mode re-points to | ||
| Cover fill on hover | ||
var(--border-s) solid var(--border-faded) |
Cover border | |
var(--space-m) var(--space-l) |
Header and footer strip inset | |
| Centred content inset | ||
-2px |
Hover travel; re-pointed to 0 under reduced motion |
|
0 7px 1px -3px var(--black-alpha-10) |
Hover shadow | |
var(--border-s) solid var(--border-faded) |
Contents list outer border | |
| Row height | ||
| Space between a row's slots | ||
| Row title; steps down at 960px | ||
| Row fill on hover |
Selectors
| Selector | Purpose |
|---|---|
| L0 grid of covers; owns columns and gap | |
.book-shelf[data-cols="3"] |
Three-column shelf |
.book-cover[data-size="small|large"] |
Smaller or larger cover — height, title, and inset together |
| Group heading above a shelf | |
| One closed book — a self-contained link card | |
| / | Full-width strips, start slot and end slot |
| Header start icon; varies per card | |
| Header end icon; hints at what clicking does | |
| Centred title and description, absorbs remaining height | |
| / | Cover text |
| Footer start — "by X" | |
| Footer end — metadata: a tag, a date, a count | |
| L1 table of contents; draws the outer border | |
| Group heading above a contents list | |
| One line in the contents | |
details.book-contents-item |
The same row as a disclosure — opens in place instead of navigating |
details.book-contents-item > summary |
The visible row line; carries the row layout, hover, and focus ring |
| Revealed content: description and meta | |
| Leading icon; varies per row | |
| Title and description column; grows to fill | |
| / | Row text |
| Metadata, inside the body under the description | |
| Trailing icon; hints at what clicking does | |
| "by X", inside meta |
Use in another product
The design system installs once per product:
npm install github:bydefaultstudio/design-system-dist#semver:^4.7.0
This component's styles ship in design-system.css. No JavaScript, nothing else to include.