Skip to content
FivePaths/microsite

FivePaths microsite framework

A stylesheet that styles HTML elements by what they are and where they sit. Write semantic markup and link the sheet, and the page gets the FivePaths layout, a light and a dark scheme, and text that clears WCAG AAA contrast.

Set up a page Read base.css

Every text and background pairing clears 7:1. The repository's contract test measures each one against the least favourable ground it can land on.

AAA
On this page

Set up a page

Link a release in the head. Everything after that is HTML.

head
<link rel="preconnect" href="https://cdn.fivepaths.com" crossorigin>
<link rel="preload" as="font" type="font/woff2" crossorigin
      href="https://cdn.fivepaths.com/microsite/v3/fonts/overpass-latin-wght-normal-2.woff2">
<link rel="stylesheet" href="https://cdn.fivepaths.com/microsite/v3/base-3.6.1.css">
<link rel="stylesheet" href="/assets/site.css">
<script src="https://cdn.fivepaths.com/microsite/v3/theme-1.0.0.js"></script>

Pin the release. A release is published once and never changes, so the sheet you reviewed is the sheet visitors receive. The current release is 3.6.1; base.css with no number follows the newest.

The body

body
<body>
<a href="#main">Skip to content</a>
<header>
  <a href="/"><span>Example<span class="tld">.org</span></span></a>
  <nav aria-label="Site">
    <a href="/guide/" aria-current="page">Guide</a>
    <a href="/about/">About</a>
    <a class="ink" href="/start/">Get started</a>
  </nav>
</header>
<main id="main">
  <section data-band="hero">
    <h1>What the site is</h1>
    <p>One sentence on what it does.</p>
  </section>
  <section>
    <header>
      <h2>A section</h2>
      <p>Its lead.</p>
    </header>
    <p>Its content.</p>
  </section>
</main>
<footer>
  <p>
    <strong>Example.org</strong>
    <span>is a</span>
    <a href="https://fivepaths.com">FivePaths</a>
    <span>product.</span>
    <span>&copy; 2026 FivePaths, LLC</span>
  </p>
  <small><p>Fine print.</p></small>
</footer>
</body>

The site's own sheet

A site's own rules go in site.css, linked after the framework. The framework's rules sit in cascade layers and a site sheet does not, so a site rule wins without extra specificity. A name the site invents carries the site's prefix.

site.css
/* What this site has and the framework does not */
[data-example-map] {
  aspect-ratio: 4 / 3;
  border: 1px solid var(--fp-rule);
  border-radius: var(--fp-radius);
}

Structure

Sections and bands

A section is a band across the page, and data-band picks its ground.

Markup
main > section > header
<header>
  <p>Structure</p>
  <h2>Sections and bands</h2>
  <p>A section is a band across the page, and <code>data-band</code> picks its ground.</p>
</header>

A p before the heading is an eyebrow, and the p after it is the lead. Give an eyebrow only to the few sections that need one.

Band values
data-bandGround
noneThe page ground.
altThe alternate ground, for every other section.
darkThe near-black ground. Everything inside re-reads its tokens and adapts.
heroThe opening band, dark, carrying the page's h1.
proofOne line of claim beside a chip, dark.
ctaThe closing band, dark and centred.
stripA thin band between two rules.

data-tight on any band gives it less vertical padding.

Wider than the column

A page laid out in the framework: a header, a dark hero, and three cards
data-width="wide" takes the wider track. "full" runs to the viewport edges and drops the frame.
Markup: Wider than the column
main > section > [data-width="wide"]
<figure data-width="wide">
  <img src="example-screen.svg" alt="A page laid out in the framework: a header, a dark hero, and three cards" width="1600" height="1000">
  <figcaption><code>data-width="wide"</code> takes the wider track. <code>"full"</code> runs to the viewport edges and drops the frame.</figcaption>
</figure>

Lists and groups

A list in main is styled by being one. data-list picks a shape that needs a grid.

A list

Markup: A list
main ul:not([data-list])
<ul>
  <li>Each item gets a drawn dot, so nothing falls back to a missing glyph.</li>
  <li>Running text is capped at a readable measure.</li>
</ul>

A list of checks

Markup: A list of checks
main ul[data-marker="check"]
<ul data-marker="check">
  <li>Both colour schemes</li>
  <li>A 44px target on every control</li>
</ul>

A numbered list

  1. Link the release.
  2. Write the markup.
  3. Check both schemes.
Markup: A numbered list
main ol:not([data-list])
<ol>
  <li>Link the release.</li>
  <li>Write the markup.</li>
  <li>Check both schemes.</li>
</ol>

Steps

  1. Pin a release

    The sheet you reviewed is the sheet you serve.

  2. Write elements

    Their position carries the layout.

  3. Run the checks

    Contrast, targets and reflow, in both schemes.

Markup: Steps
ol[data-list="steps"]
<ol data-list="steps">
  <li><h3>Pin a release</h3><p>The sheet you reviewed is the sheet you serve.</p></li>
  <li><h3>Write elements</h3><p>Their position carries the layout.</p></li>
  <li><h3>Run the checks</h3><p>Contrast, targets and reflow, in both schemes.</p></li>
</ol>

Cards

Markup: Cards
ul[data-list="cards"]
<ul data-list="cards" data-cols="3">
  <li><a href="#tokens">
    <h3>A card that links</h3>
    <p>The link fills the card, so the whole card is the target.</p>
    <span>See the tokens</span>
  </a></li>
  <li><div>
    <h3>A card that does not</h3>
    <p>Its content sits in a div and gets the same padding.</p>
  </div></li>
  <li><div>
    <h3>Three across</h3>
    <p><code>data-cols</code> sets the count once there is room for it.</p>
  </div></li>
</ul>

Chips

Markup: Chips
ul[data-list="chips"]
<ul data-list="chips">
  <li>base.css</li>
  <li>tokens.json</li>
  <li>theme-1.0.0.js</li>
</ul>

Logos

Markup: Logos
ul[data-list="logos"]
<ul data-list="logos" data-width="full">
  <li style="--logo-h: 2.4rem"><a href="https://fivepaths.com"><img src="/brand/fivepaths/fivepaths-logo.svg" alt="FivePaths" width="108" height="111"></a></li>
</ul>

A logo row runs to the page edges. Marks drawn by different people share nothing but their height, so each item sets its own in --logo-h.

Actions

Set up a page Read base.css
Markup: Actions
[data-actions]
<div data-actions>
  <a href="#start">Set up a page</a>
  <a href="base.css">Read base.css</a>
</div>

The first link is the primary action and takes the fill; the rest are secondary. A second primary needs a second group. ghost, ink and teal name a variant where position cannot.

Layouts and blocks

A two-column layout is named in data-layout. A figure is framed by what it holds.

Split panels

One option

Each child is a panel.

The other

data-band="dark" on a panel re-points its tokens, so it reads correctly with no rules of its own.

Markup: Split panels
[data-layout="split"]
<div data-layout="split">
  <div>
    <h3>One option</h3>
    <p>Each child is a panel.</p>
  </div>
  <div data-band="dark">
    <h3>The other</h3>
    <p><code>data-band="dark"</code> on a panel re-points its tokens, so it reads correctly with no rules of its own.</p>
  </div>
</div>

Feature rows

Copy on one side

Two columns that stack below 56rem. flip swaps them, uneven weights them, and top aligns them to the top.

A page laid out in the framework: a header, a dark hero, and three cards
Markup: Feature rows
[data-layout~="feature"]
<div data-layout="feature">
  <div>
    <h3>Copy on one side</h3>
    <p>Two columns that stack below 56rem. <code>flip</code> swaps them, <code>uneven</code> weights them, and <code>top</code> aligns them to the top.</p>
  </div>
  <figure>
    <img src="example-screen.svg" alt="A page laid out in the framework: a header, a dark hero, and three cards" width="1600" height="1000">
  </figure>
</div>

A table

Release files
FileChanges
base-3.6.1.cssNever
base.cssWith each release
tokens.jsonWith each release
Markup: A table
figure:has(> table)
<figure tabindex="0" aria-label="Release files">
  <table>
    <caption>Release files</caption>
    <thead><tr><th scope="col">File</th><th scope="col">Changes</th></tr></thead>
    <tbody>
      <tr><th scope="row">base-3.6.1.css</th><td>Never</td></tr>
      <tr><th scope="row">base.css</th><td>With each release</td></tr>
      <tr><th scope="row">tokens.json</th><td>With each release</td></tr>
    </tbody>
  </table>
</figure>

A table scrolls sideways inside its figure, so the figure takes tabindex="0" and a label to be reachable from the keyboard.

A quotation

This sheet is for generated markup, where the component set is closed and a checker fails a build whose structure does not match.

base.css, header comment
Markup: A quotation
figure:has(> blockquote)
<figure>
  <blockquote><p>This sheet is for generated markup, where the component set is closed and a checker fails a build whose structure does not match.</p></blockquote>
  <figcaption>base.css, header comment</figcaption>
</figure>

A code listing

site.css
main { color: var(--fp-ink); }
Markup: A code listing
figure:has(> pre)
<figure>
  <figcaption>site.css</figcaption>
  <pre tabindex="0"><span class="k">main</span> { <span class="m">color</span>: <span class="p">var(--fp-ink)</span>; }</pre>
</figure>

The figcaption is the title bar. The classes c, k, m and p on its spans come from a highlighter, for comments, keys, attributes and values.

A definition list

Band
The ground a section sits on.
Measure
The cap on a line of running text, in ch.
Markup: A definition list
dl
<dl>
  <dt>Band</dt>
  <dd>The ground a section sits on.</dd>
  <dt>Measure</dt>
  <dd>The cap on a line of running text, in <code>ch</code>.</dd>
</dl>

Callouts

Markup: Callouts
aside[data-severity]
<aside>
  <p>Note</p>
  <p>The first paragraph is the label, so the severity is written as well as coloured.</p>
</aside>
<aside data-severity="warning">
  <p>Warning</p>
  <p>A published release is never revised. Cut a new one and move the pin.</p>
</aside>
<aside data-severity="critical">
  <p>Important</p>
  <p>Every colour is a token. A colour written as a literal misses the dark scheme and the dark bands.</p>
</aside>

A disclosure

What the triangle shows

It points right while the panel is closed and down once it is open.

Markup: A disclosure
details > summary
<details>
  <summary>What the triangle shows</summary>
  <p>It points right while the panel is closed and down once it is open.</p>
</details>

A link in a sentence, like this one to the tokens, keeps the height of its line; data-target marks a link that stands alone.

tokens.json base.css

Markup: Links
main a[data-target]
<p>A link in a sentence, like this one to <a href="#tokens">the tokens</a>, keeps the height of its line; <code>data-target</code> marks a link that stands alone.</p>
<p><a data-target href="tokens.json">tokens.json</a> <span aria-hidden="true">·</span> <a data-target href="base.css">base.css</a></p>

A link marked with data-target gets a 44px target. The sheet also gives one to a link that is the only element in its paragraph, list item or definition, which is why the first paragraph here holds a code as well.

Chips, blips and fine print

v3 Blue Green Red Orange Purple

Fine print is a small element, in the muted ink.
Markup: Chips, blips and fine print
[data-chip], [data-route], small
<p>
  <span data-chip>v3</span>
  <span data-route="blue">Blue</span>
  <span data-route="green">Green</span>
  <span data-route="red">Red</span>
  <span data-route="orange">Orange</span>
  <span data-route="purple">Purple</span>
</p>
<small>Fine print is a small element, in the muted ink.</small>

A route blip is the one place colour carries meaning, for data such as a transit line. It is never chrome.

Forms

A form that names its layout gets styled labels, fields and choices. A form without data-layout is left as the browser draws it.

A column of fields

Only used to reply.
Reply by
Markup: A column of fields
form[data-layout="stack"]
<form data-layout="stack" action="#forms">
  <div><label for="s-name">Name</label><input id="s-name" name="name" autocomplete="name"></div>
  <div><label for="s-email">Email</label><small id="s-email-hint">Only used to reply.</small><input id="s-email" type="email" name="email" autocomplete="email" aria-describedby="s-email-hint"></div>
  <div><label for="s-topic">About</label>
    <select id="s-topic" name="topic"><option>A correction</option><option>The data</option><option>Something else</option></select></div>
  <div><label for="s-message">Message</label><textarea id="s-message" name="message"></textarea></div>
  <fieldset>
    <legend>Reply by</legend>
    <label><input type="radio" name="reply" value="email" checked> Email</label>
    <label><input type="radio" name="reply" value="phone"> Phone</label>
  </fieldset>
  <label><input type="checkbox" name="copy"> Send me a copy</label>
  <div data-actions><button type="submit">Send</button><button type="reset">Clear</button></div>
</form>

A field is a div holding its label, a hint in small if it has one, and then its control. A fieldset groups radios or checkboxes, and each choice sits inside its label, which is the 44px target. A reset button is the secondary one.

Filters

Every word must appear.
Markup: Filters
form[data-layout="filters"]
<form data-layout="filters" action="#forms" role="search" aria-label="Filter records">
  <div><label for="f-kind">Kind of item</label>
    <select id="f-kind" name="kind"><option value="">Any</option><option>Ordinance</option><option>Resolution</option><option>Agreement</option></select></div>
  <div><label for="f-from">From</label><input id="f-from" type="date" name="from"></div>
  <div><label for="f-q">Words in the title or the report</label><small id="f-q-hint">Every word must appear.</small><input id="f-q" type="search" name="q" aria-describedby="f-q-hint"></div>
  <div data-actions><button type="submit">Show records</button><button type="reset">Reset</button></div>
</form>

The fields wrap across the card as room runs out. A control is the last thing in its field, so the controls in a row stay on one line whatever is written above them. A row of buttons takes a line of its own.

Errors

Error: Enter an email address, like name@example.com
Reply by Error: Choose how to be replied to
Markup: Errors
[aria-invalid="true"], small[data-severity="critical"]
<form data-layout="stack" action="#forms" novalidate>
  <aside data-severity="critical" tabindex="-1">
    <p>There is a problem</p>
    <ul>
      <li><a href="#e-email">Enter an email address, like name@example.com</a></li>
      <li><a href="#e-reply-email">Choose how to be replied to</a></li>
    </ul>
  </aside>
  <div><label for="e-email">Email</label>
    <small id="e-email-error" data-severity="critical"><span class="visually-hidden">Error: </span>Enter an email address, like name@example.com</small>
    <input id="e-email" type="email" name="email" value="name.example.com" autocomplete="email" required aria-invalid="true" aria-describedby="e-email-error"></div>
  <fieldset aria-describedby="e-reply-error">
    <legend>Reply by</legend>
    <small id="e-reply-error" data-severity="critical"><span class="visually-hidden">Error: </span>Choose how to be replied to</small>
    <label><input id="e-reply-email" type="radio" name="e-reply" value="email" required> Email</label>
    <label><input type="radio" name="e-reply" value="phone" required> Phone</label>
  </fieldset>
  <div data-actions><button type="submit">Send</button></div>
</form>

An error is written between its label and its control, and the control names it in aria-describedby. The control, a group's fieldset and the message repeat it in --fp-critical, and :user-invalid draws the same border once someone has typed. The summary at the top links to each field. When the page comes back with errors, move focus to the summary, which is what its tabindex="-1" is for. Here a short script does that when you press Send, clears an error once its field is fixed, and moves focus to a field from its summary link.

A pager

Markup: A pager
nav ul[data-list="chips"] [aria-current]
<nav aria-label="Pages">
  <ul data-list="chips">
    <li><a href="#forms" rel="prev">Previous</a></li>
    <li><a href="#forms">1</a></li>
    <li><a href="#forms" aria-current="page">2</a></li>
    <li><a href="#forms">3</a></li>
    <li><a href="#forms" rel="next">Next</a></li>
  </ul>
</nav>

A pager is a chip list in a nav. The page in view is marked in weight and ink rather than colour, and a chip in a nav is at least 44px wide, so a single digit is a full target. Here the chips move aria-current when you choose one, and the markup follows.

Tokens and schemes

Every colour, size and face is a custom property. A site on another brand sets the tokens and keeps every component.

The tokens a site sets
TokenGroupLightDark
--fp-bglight#ffffff#10141c
--fp-inklight#141821#edf0f6
--fp-accentlight#006060#4dd2d2
--fp-boardboard#10141c#10141c
--fp-amberfill#ffb300#ffb300
--fp-sanstype"Overpass", …"Overpass", …

Set a group wholly or leave it alone. The rest of each group is tuned against these, and the contract test measures every pairing between them; set half a group and the pairings stop holding. tokens.json lists each group's members and the pairings that have to hold.

Light and dark

The page follows the reader's system setting. data-theme on the html element pins one scheme.

html
<html lang="en" data-theme="dark">

theme-1.0.0.js runs the header button that switches schemes. Its two labels name the scheme each one switches to, and the sheet shows one at a time.

button
<button type="button">
  <svg data-scheme="dark" aria-hidden="true" viewBox="0 0 24 24">…</svg>
  <svg data-scheme="light" aria-hidden="true" viewBox="0 0 24 24">…</svg>
  <span data-scheme="dark">Dark theme</span>
  <span data-scheme="light">Light theme</span>
</button>

The full contract

MARKUP.md in the fivepaths-cdn repository covers every element, and tokens.json holds the token contract in a form a build tool can read.

tokens.json base.css