# Atrium design system (v0.1)
---
Plain, neutral styles for Atrium app screens inside COHO. Load atrium.css, put class="at-app" on
, and use only at-* classes and --at-* tokens.
---
# About the design system
What the Atrium design system is, who it is for, and how it relates to COHO.
The Atrium design system gives every Atrium app the same plain, dependable building blocks for screens shown **inside COHO**: quick views, full app pages and settings. Managers move between COHO and many apps from many developers in a day. When those apps share one set of buttons, forms, tables and messages, managers already know how to use each new one.
> **Why does it not look like COHO?** COHO's own styling is built for COHO's product and is maintained by COHO's developers. Atrium apps need something simpler that any developer, or any AI tool, can apply in minutes, and that sits quietly inside the COHO frame. So this system is deliberately calm: warm greys, one deep blue for actions, the system font and softened corners. It fits in without pretending to be COHO.
## What is in it
| Part | What it gives you |
|---|---|
| `atrium.css` | One stylesheet: tokens and every component class |
| `tokens.json` | The same tokens in W3C design-token format, for design tools |
| Foundations | Colour, typography, spacing, elevation, content and layout rules |
| Components | Twenty-one building blocks, each with anatomy, states, usage and code, and a release status |
| Patterns | Whole screens assembled from the components |
| Rules | The short list every screen must follow |
## How it changes
The system is versioned (this is **v0.1**). New components and fixes are listed on [What's new](../updates/). Class names do not change within a major version, so a screen built today keeps working.
| Status | What it means |
|---|---|
| **Stable** | Ready to build on. Class names and behaviour will not change within this major version. |
| **Beta** | Usable, but may still change after feedback. Check What's new before you ship. |
| **Deprecated** | Still works, but is being replaced. The component page says what to use instead. |
---
# Principles
The four ideas behind every decision in the system.
- **Plain**: Nothing decorative. Every colour, border and shadow is there to make something clearer.
- **Familiar**: Native HTML elements, the system font, conventions managers already know from the web.
- **Accessible**: WCAG 2.2 AA out of the box: contrast, labels, focus and keyboard use are built in.
- **One way**: Each thing has one recommended way to build it, so apps built by different people, and by AI, come out the same.
---
# Rules
The short list every Atrium screen shown inside COHO must follow. Checked when an app asks for a public listing.
These are required. Everything else in the system is a strong recommendation.
| Rule | Why |
|---|---|
| Load `atrium.css` and put `at-app` on the body | Every app starts from the same neutral base |
| Use the `--at-*` tokens by meaning; do not override them with brand colours | Screens stay readable and consistent, in light and dark mode |
| No external web fonts | Faster screens, no third-party tracking |
| Every input has a visible label | Placeholders disappear and are not read reliably |
| Never rely on colour alone | Status tags and alerts always say what they mean in words |
| Keep the focus outline | Keyboard users need to see where they are |
| One primary button per view | Managers always know the main action |
| Do not disable submit buttons | Let managers press it, then show what to fix. A greyed-out button gives no reason |
| Buttons act, links go | A button does something on this screen; a link goes somewhere else. Screen readers announce them differently |
| Confirm destructive actions | Disconnecting or deleting is never one click |
| No close button of your own in a quick view | COHO draws the frame and its close control |
## What you are free to do
- Add your own small logo or mark beside the page title.
- Use your own illustrations in empty states.
- Build components the system does not cover, from the tokens.
---
# Accessibility
What every Atrium screen must get right for keyboard, screen reader and low-vision users.
The components pass WCAG 2.2 AA when used as shown. The checks below are what reviewers look for.
| Check | How |
|---|---|
| Contrast | Use the colour tokens; every text pair is 4.5:1 or more |
| Labels | Every input has a visible label linked with for / id |
| Errors | Message beside the field, `aria-invalid` and `aria-describedby` |
| Keyboard | Everything works with Tab, Enter, Space and Escape |
| Focus | The 3px focus outline is never removed |
| Colour | Status is always said in words, not only shown in colour |
| Motion | Spinners and skeletons stop when reduced motion is on |
| Structure | One h1, headings in order, real buttons and links |
---
# Using it with AI
Hand the whole design system to an AI coding tool in one step.
Every page on this site is also published as plain Markdown, written so an AI tool can read it with no noise. The quickest way is to give your tool the whole system in one file.
| File | Use it for |
|---|---|
| [`llms-full.txt`](../../llms-full.txt) | The whole design system in one file. Paste the link into your AI tool, or download it as `DESIGN.md`. |
| [`llms.txt`](../../llms.txt) | An index of every page, for agents that fetch only what they need |
| `.md` | Any single page as Markdown: use **Copy as Markdown** at the top of each page |
| [`atrium.css`](../../atrium.css) | The stylesheet itself |
| [`tokens.json`](../../tokens.json) | Tokens in W3C design-token format |
Add to your prompt:
```text
Use only atrium.css classes (at-*) and --at-* tokens for every screen.
Follow the Atrium design system: https://docs.coho.life/atrium-design/llms-full.txt
```
---
# What's new
Changes to the design system, newest first.
| Version | Date | Changes |
|---|---|---|
| v0.1 | Oct 2026 | First release: colour tokens named by role, four elevation levels, content guidelines, 21 components (AI content in beta), 4 patterns, light and dark mode, state hooks (`at-is-hover`, `at-is-active`, `at-is-focus`). |
Spotted a gap, or need a component that is not here? Tell us through the Atrium developer docs **Get help** page.
---
# Colour
Colours are named by their job. Pick the role that matches what you mean, and the right colour follows, in light and dark mode.
Mostly warm greys, one deep blue for actions, and a small set of colours that each mean one thing. The blue is deliberately not COHO's brand pink, so an app never looks like part of COHO's own navigation.
> **Why not use my brand colours?** Inside COHO, managers rely on colour to mean the same thing everywhere: blue is an action, red is a problem. Your brand can appear in your logo and illustrations, but not in buttons, links or status.
## How tokens are named
Every colour token follows one pattern: **property**, then **role**, then **emphasis**. `--at-bg-danger-subtle` is a background, for danger, in the subtle strength. Once you know the pattern you can guess any token, and so can an AI tool.
| Part | Options | Example |
|---|---|---|
| Property | `text`, `bg`, `border`, `surface`, `shadow` | `--at-text-...` |
| Role | `primary`, `neutral`, `info`, `success`, `warning`, `danger`, `discovery`, `inverse`, `accent-` | `--at-text-danger` |
| Emphasis | `subtle` or `bold` (left out for the usual strength) | `--at-bg-danger-subtle` |
## Roles
| Role | Use for | Never for |
|---|---|---|
| **primary** | The main action: primary buttons, links, the selected tab | Status or decoration |
| **neutral** | Everything without a meaning: text, borders, secondary buttons, table headers | |
| **info** | Neutral news, or something in progress | Errors or success |
| **success** | Done and fine: paid, saved, connected | Anything still to do |
| **warning** | Needs attention soon, nothing has failed | Errors |
| **danger** | Errors, overdue, and actions that delete or disconnect | Emphasis or branding |
| **discovery** | Something new: onboarding, a new feature, AI-drafted content | Status |
| **inverse** | Text on bold backgrounds and primary buttons | Text on the page |
| **accent** | Categories with no meaning, such as property types or charts | Anything with a meaning: use a status role |
> **The accent test** If you could swap one accent colour for another and the screen would mean exactly the same, use an accent. If the colour says something (good, bad, new), use a role instead.
## Text
### Text
| Token | Light | Dark | Use |
|---|---|---|---|
| `--at-text` | #1c1917 | #f5f5f4 | Body text and headings |
| `--at-text-subtle` | #57534e | #a8a29e | Hints, secondary text, table headers |
| `--at-text-primary` | #3d4ed8 | #8e9bfa | Links and link buttons |
| `--at-text-info` | #2f5fd0 | #7aa2f7 | Information text |
| `--at-text-success` | #067647 | #47cd89 | Paid, done, connected |
| `--at-text-warning` | #b54708 | #fdb022 | Due soon, needs attention |
| `--at-text-danger` | #b42318 | #f97066 | Errors, overdue, destructive actions |
| `--at-text-discovery` | #6941c6 | #b692f6 | New things, onboarding, AI |
## Backgrounds
### Neutral and primary
| Token | Light | Dark | Use |
|---|---|---|---|
| `--at-bg-primary` | #3d4ed8 | #8e9bfa | Primary buttons |
| `--at-bg-neutral` | #f6f5f3 | #262421 | Table headers, skeletons, muted cards |
| `--at-bg-neutral-pressed` | #d6d3d1 | #44403c | Secondary button pressed |
### Subtle: alerts, tags, highlights
| Token | Light | Dark | Use |
|---|---|---|---|
| `--at-bg-info-subtle` | #eff4ff | #18233d | Information alert and tag |
| `--at-bg-success-subtle` | #ecfdf3 | #10291d | Success alert and tag |
| `--at-bg-warning-subtle` | #fffaeb | #332410 | Warning alert and tag |
| `--at-bg-danger-subtle` | #fef3f2 | #3a1a17 | Error alert and tag, danger button hover |
| `--at-bg-discovery-subtle` | #f4f3ff | #22183d | Discovery alert and tag, AI content |
### Bold: the one thing that most needs attention (pair with --at-text-inverse)
| Token | Light | Dark | Use |
|---|---|---|---|
| `--at-bg-info-bold` | #2f5fd0 | #7aa2f7 | Bold information tag |
| `--at-bg-success-bold` | #067647 | #47cd89 | Bold success tag |
| `--at-bg-warning-bold` | #b54708 | #fdb022 | Bold warning tag |
| `--at-bg-danger-bold` | #b42318 | #f97066 | Bold danger tag (Overdue, Failed) |
| `--at-bg-discovery-bold` | #6941c6 | #b692f6 | Bold discovery tag (New) |
## Borders
### Borders
| Token | Light | Dark | Use |
|---|---|---|---|
| `--at-border` | #d6d3d1 | #44403c | Borders and dividers |
| `--at-border-bold` | #57534e | #a8a29e | Hovered inputs |
| `--at-border-focus` | #3d4ed8 | #8e9bfa | Focus outline |
| `--at-border-danger` | #b42318 | #f97066 | Error fields, danger buttons, error alert edge |
## Accent hues
Six hues with no meaning, each with a background and a text token: `--at-bg-accent-` and `--at-text-accent-`. Use them through the accent [tags](../../components/tag/) or for chart series.
```html
HMOStudioHouseFlatStudentCommercial
```
## Rules
- Pick tokens by meaning, never by the colour you want. A green you like is not a reason to use `success`.
- Use **bold** for one thing per view at most, such as an Overdue tag in a list of otherwise subtle tags.
- Every text and background pair here passes WCAG AA (4.5:1) in light and dark mode. Mixing roles (danger text on a success background) is not checked: do not do it.
## Dark mode
Every colour token has a dark value, applied inside `[data-color-mode="dark"]`. If your app only uses the tokens, dark mode works with no changes. Use the **Light / Dark** switch on any example to see it.
---
# Typography
The system font, five sizes and three weights.
The system font stack: fast, familiar on every device, and no web font to download.
```html
Page title, 24px
Section heading, 20px
Sub-heading, 16px bold
Body text, 16px, line height 1.5.
Small muted text, 14px, for hints.
```
| Token | Size | Use for |
|---|---|---|
| `--at-text-xl` | 24px | Page title (h1) and the page header title |
| `--at-text-lg` | 20px | Section heading (h2) |
| `--at-text-md` | 16px | Body text, inputs |
| `--at-text-sm` | 14px | Buttons, tables, hints |
| `--at-text-xs` | 12px | Tags |
Weights: `--at-weight-regular` (400) for text, `--at-weight-bold` (600) for labels, buttons and emphasis, and `--at-weight-heavy` (700) for headings only, so the top of every screen is clear. Line height: `--at-line-height` (1.5). Helpers: `at-small`, `at-muted`. Do not load web fonts, use other weights, or write in capitals.
---
# Spacing and shape
An eight-step spacing scale and three corner radii.
| Token | Size | Typical use |
|---|---|---|
| `--at-space-1` | 2px | Tag padding |
| `--at-space-2` | 4px | Gap inside a field |
| `--at-space-3` | 8px | Inline gaps, input padding |
| `--at-space-4` | 12px | Table cell padding |
| `--at-space-5` | 16px | Stack gaps, card and page padding |
| `--at-space-6` | 24px | Dialog padding |
| `--at-space-7` | 32px | Empty state padding |
| `--at-space-8` | 48px | Large separations |
Radius: `--at-radius-0` (0), `--at-radius-1` (6px: inputs, buttons, tags), `--at-radius-2` (10px: cards, dialogs, empty states). Corners are softened slightly so screens feel friendly without looking like a toy. Shadows belong to [elevation](../elevation/). Prefer the [stack and inline](../../components/layout/) helpers to spacing things by hand.
---
# Elevation
Four surface levels, each paired with its shadow, so depth means the same thing on every screen.
Elevation shows what sits on top of what. Atrium has four levels. Each raised level pairs a surface token with a shadow token: always use them together.
| Level | Tokens | Use for |
|---|---|---|
| **Sunken** | `--at-surface-sunken` | A backdrop that groups other content, such as a column of cards |
| **Default** | `--at-surface` | The page itself, inputs and secondary buttons |
| **Raised** | `--at-surface-raised` + `--at-shadow-raised` | Cards |
| **Overlay** | `--at-surface-overlay` + `--at-shadow-overlay` | Dialogs and menus that sit over everything else |
```html
Raised card
On a sunken backdrop.
```
## Rules
- Always pair a raised or overlay surface with its own shadow.
- Do not raise things just for emphasis. Most content sits flat on the page; use a card only to group.
- Never put a card inside a card, or a shadow on a shadow.
- In dark mode, higher surfaces are **lighter**, because shadows are hard to see on dark backgrounds. The tokens handle this for you.
---
# Content
How Atrium screens talk to managers: tone for each moment, the words to use, and how to write dates and money.
Managers use COHO all day, often between viewings, calls and repairs. Write so they can read a screen in a glance and act. Plain words, short sentences, the most important thing first.
## Tone for each moment
Your voice stays the same; your tone changes with how the manager is likely to feel. The more stressful the moment, the plainer and calmer the words.
| Moment | How it should sound | Write | Not |
|---|---|---|---|
| Error | Calm and specific. Say what happened and what to do. Never blame. | "Could not send reminders. 3 tenants have no email address." | "Oops! Something went wrong." |
| Destructive action | Serious and exact. Name the thing and the result. | "Disconnect Rent Radar? It stops receiving updates." | "Are you sure?" |
| Warning | Direct, not alarming. Say when it matters. | "Two tenancies end this week." | "URGENT: action required!!" |
| Success | Short and quiet. Confirm, then get out of the way. | "Settings saved." | "Woohoo! You're all set!" |
| Empty | Helpful. Say why it is empty and what fills it. | "No meter reads yet. Reads appear once tenants submit them." | "Nothing to see here." |
| First use | Welcoming but brief. One sentence on what the app does, then the first step. | "Connect your supplier to see readings here." | Three paragraphs about your company. |
| AI output | Honest. Say it was drafted by AI and needs checking. | "Drafted by AI. Check before sending." | Presenting AI text as fact. |
## Words to use
Use the words COHO uses, so managers never have to translate between your app and the rest of COHO.
| Use | Not |
|---|---|
| manager | user, admin, customer |
| tenant | resident, occupant, renter |
| tenancy | lease, contract, agreement |
| property | building, unit, asset (a room in a property is a **room**) |
| owner (the person who owns a property) | landlord, client |
| rent | payment, charge (unless it is not rent) |
| log in (verb), login (noun) | sign in, signon |
| connect / disconnect | install / uninstall, enable / disable (for apps) |
## Writing style
- **British English**: colour, organise, cancelled.
- **Sentence case** everywhere: "Send reminder", not "Send Reminder". Capitals only for names and proper nouns.
- **No full stops** in titles, labels, buttons or tags. Use them in sentences in body text, hints and messages.
- **Address the manager as "you"**. Use "we" only for COHO, sparingly.
- **Lead with the verb** on buttons and links: "Book inspection", "View history".
- **No exclamation marks, jokes or emoji.** Managers may be reading your error after a bad day.
## Formats
| Thing | Write | Not |
|---|---|---|
| Date | 4 Oct 2026, or 4 October 2026 in sentences | 04/10/2026, 10/4/26, 4th October |
| Date range | 1 Sep 2026 to 31 Aug 2027 | 1/9/26 - 31/8/27 |
| Time | 9:30am, 2pm | 09:30, 14:00hrs |
| Money | £650.00, £1,250.00 (right-aligned in tables) | 650, GBP 650, £650 |
| Money per period | £650.00 per month | £650/m, £650 pcm |
| Numbers | 1 to 9 as figures in UI (3 tenants), commas from 1,000 | three tenants, 1000 |
| Phone | 07700 900123, 020 7946 0000 | +447700900123 |
| Relative time | Updated 5 minutes ago (for under a day), then the date | Updated 1,440 minutes ago |
> **Buttons act, links go** A button does something on this screen: save, send, delete. A link takes the manager somewhere else. Label buttons with the action and links with the destination, and do not style one as the other.
---
# Layout in COHO
Where your screens appear inside COHO, and how wide they can be.
| Where | Width | Design for |
|---|---|---|
| Quick view (side) | 360px to 480px | One column. Few table columns, or a summary list. |
| Modal | Up to about 960px | Two columns at most. |
| Full app page | The whole content area | Normal page layout, max reading width about 72 characters. |
COHO draws the frame, the title bar and the close control around your screen. Start your screen with a [page header](../../components/page-header/), and keep `at-app` padding (16px) as the outer margin.
---
# Button
Status: stable
Buttons do things. Links go places.
Buttons let managers act: save, send, connect, disconnect. Every screen should make it obvious which things can be clicked and what will happen. There are four kinds of button, one for each level of emphasis, so the most important action always stands out.
Buttons are at least 36px tall. On touch screens, keep at least 8px between buttons so each one is easy to hit.
## Example
```html
```
## Anatomy
1. **Label**: font-family: var(--at-font); font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold) /* 600 */; color: see States below
2. **Container**: min-height: 36px; border-radius: var(--at-radius-1) /* 6px */; border: 1px solid; background: see States below
3. **Border (secondary)**: border-color: var(--at-border); background: var(--at-surface)
## Size and spacing
- **Size**: min-height: 36px; min-width: none (fits the label)
- **Padding**: block: var(--at-space-3) /* 8px */; inline: var(--at-space-5) /* 16px */
- **Spacing**: gap between buttons: var(--at-space-3) /* 8px */; gap label to icon: var(--at-space-2) /* 4px */
## Variants
### Primary button (emphasis: High)
For the one main action on a screen or in a dialog: Save, Send, Connect. Never more than one per view.
```html
```
| State | Specification |
|---|---|
| Default | background: var(--at-bg-primary); color: var(--at-text-inverse) |
| Hover | background: var(--at-bg-primary); filter: brightness(0.92) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Disabled | opacity: 0.5; cursor: not-allowed; avoid: see Usage |
### Secondary button (emphasis: Medium)
The default. For every other action, and for actions repeated in a list.
```html
```
| State | Specification |
|---|---|
| Default | background: var(--at-surface); border-color: var(--at-border); color: var(--at-text) |
| Hover | background: var(--at-bg-neutral) |
| Active | background: var(--at-border) |
| Disabled | opacity: 0.5 |
### Danger button (emphasis: Medium)
For actions that delete or disconnect. Always ask for confirmation in a dialog first.
```html
```
| State | Specification |
|---|---|
| Default | color: var(--at-text-danger); border-color: var(--at-border-danger) |
| Hover | background: var(--at-bg-danger-subtle) |
| Disabled | opacity: 0.5 |
### Link button (emphasis: Low)
For low-emphasis actions inside tables and text, such as View or Edit on a row.
```html
```
| State | Specification |
|---|---|
| Default | color: var(--at-text-primary); background: transparent |
| Hover | text-decoration: underline |
## When to use
- Primary: the one main action in a view.
- Secondary: every other action.
- Danger: delete or disconnect, always confirmed.
- Link: low-emphasis actions in tables and text.
## When not to use
- To go to another page: use a link. Buttons act, links go.
- More than one primary button in the same view.
## Content
- Start with a verb: "Save changes", "Send reminder", "Connect".
- Use sentence case and keep to three words or fewer.
- Say what will happen, not "OK" or "Submit".
## Do and don't
Do: One primary button, last in the row, labelled with what it does.
```html
```
Don't: Two primary buttons, and labels that do not say what happens.
```html
```
Do: Save stays pressable. Pressing it shows what to fix.
```html
Enter a price, like 12.50
```
Don't: A greyed-out Save with no reason why. Keyboard and screen reader users may not find it at all.
```html
```
## Do
- Keep buttons pressable. Validate when pressed and show what to fix (see Form submit and validation).
- While an action runs, keep the label and add a spinner, rather than disabling the button.
- Right-align buttons in dialogs, cards and short tasks, with the primary button last.
- Left-align buttons at the end of long, full-page forms, with the primary button first.
## Don't
- Disable a submit button to stop a form being sent. The one exception: Previous and Next at the ends of a list (see Pagination).
- Use a button to go to another page, or a link to save or delete.
- Put a tooltip on a disabled button: keyboard users cannot reach it.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-button` | Secondary (default) |
| `at-button--primary` | Main action, accent fill |
| `at-button--danger` | Destructive action |
| `at-button--link` | Looks like a link |
| `disabled / aria-disabled="true"` | Unavailable |
## Tokens
`--at-border`, `--at-surface`, `--at-text`, `--at-bg-neutral-hovered`, `--at-bg-neutral-pressed`, `--at-border-primary`, `--at-bg-primary`, `--at-text-inverse`, `--at-border-danger`, `--at-text-danger`, `--at-bg-danger-subtle`, `--at-text-primary`
## Accessibility
- Use button for actions and a for links.
- Focus shows a 3px outline; do not remove it.
---
# Checkbox
Status: stable
Turn one option on or off, or pick several from a list.
Checkboxes turn a single option on or off, or let managers pick any number of options from a list. Each checkbox has its own label, and a group of them sits in a fieldset with a legend that asks the question.
Atrium uses the browser's own checkbox, tinted with the accent colour. It has no separate hover style.
## Example
```html
```
## Anatomy
1. **Legend**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold); margin-bottom: var(--at-space-2) /* 4px */
2. **Box**: width: 1.125rem; height: 1.125rem /* 18px */; accent-color: var(--at-bg-primary)
3. **Label**: font-size: var(--at-text-md) /* 16px */; the whole label is clickable
## Size and spacing
- **Box**: 18px by 18px; margin-top: 0.15rem to line up with the first line of text
- **Spacing**: box to label: var(--at-space-3) /* 8px */; between options: var(--at-space-3) /* 8px */
## Variants
### Checkbox group
Pick any number of options. Wrap the group in a fieldset with a legend.
```html
```
| State | Specification |
|---|---|
| Unchecked | browser default box |
| Checked | fill: var(--at-bg-primary) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Disabled | greyed by the browser |
### Single checkbox
One option on its own, for a yes or no setting. No fieldset is needed.
```html
```
## When to use
- Choosing any number of options, including none.
- A single yes or no setting that is saved with the form.
## When not to use
- Exactly one choice from a list: use radios.
- A setting that takes effect straight away: use a button.
## Content
- Labels say what is turned on: "Email", "Include archived properties".
- Avoid negatives such as "Do not send reminders".
- The legend asks the question: "Send reminders by".
## Do and don't
Do: A positive label: ticked means on.
```html
```
Don't: A double negative: ticked means... what?
```html
```
## Do
- Wrap the input in its label so the text is clickable.
## Don't
- Use a checkbox to trigger an action immediately.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-fieldset` | Groups related choices |
| `at-choice` | Label wrapping the input and its text |
## Tokens
`--at-bg-primary`
## Accessibility
- Group related checkboxes in a fieldset with a legend.
---
# Date input
Status: stable
A date, using the browser date picker.
Date inputs use the browser's own date picker, so managers get the calendar they already know on desktop and phone. They use the same at-input class as a text input, with type="date".
The value is always sent as YYYY-MM-DD, whatever the browser shows on screen. Show dates back to managers as "4 Oct 2026".
## Example
```html
```
## Anatomy
1. **Label**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
2. **Hint (optional)**: font-size: var(--at-text-sm); color: var(--at-text-subtle)
3. **Field**: class: at-input, type="date"; min-height: 36px; calendar icon and picker: drawn by the browser
## Size and spacing
- **Field**: min-height: 36px; width: 100% of its container
- **Limits**: min="2026-01-01" and max="..." stop out-of-range dates in the picker
## Variants
### Date input
Set min and max when only some dates make sense, for example no start dates in the past.
```html
```
| State | Specification |
|---|---|
| Default | border: 1px solid var(--at-border) |
| Hover | border-color: var(--at-text-subtle) |
| Focus | outline: 3px solid var(--at-border-focus) |
| Error | border: 2px solid var(--at-border-danger); plus an .at-error message |
| Disabled | opacity: 0.5 |
## When to use
- Any single date: a start date, a due date, an inspection date.
## When not to use
- A date of birth or other date far in the past: three text inputs (day, month, year) are quicker.
- A month only: use a select.
## Content
- Label with what the date is for: "Start date", not "Date".
- Error messages give an example: "Enter a date after 5 Oct 2026".
## Do and don't
Do: The browser picker, labelled with what the date is for.
```html
```
Don't: A text box with a US-format placeholder.
```html
```
## Do
- Use the native date input.
## Don't
- Add a third-party date picker.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-input with type="date"` | Native date input |
## Tokens
`--at-border`, `--at-surface`, `--at-text`, `--at-border-bold`
## Accessibility
- The native input supports keyboard entry.
---
# Field: label, hint and error
Status: stable
Every form control sits in a field: label, optional hint, the control, and an error when it is wrong.
A field wraps every form control: the label, an optional hint, an error message when the answer is wrong, and the control itself. Using it everywhere keeps forms evenly spaced and makes errors look the same on every screen.
Show errors after the manager tries to save, not while they are still typing.
## Example
```html
In pounds, before VAT.
Enter a price, like 12.50
```
## Anatomy
1. **Label (.at-label)**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
2. **Hint (.at-hint)**: font-size: var(--at-text-sm); color: var(--at-text-subtle)
3. **Error (.at-error)**: font-size: var(--at-text-sm); font-weight: var(--at-weight-bold); color: var(--at-text-danger)
4. **Control**: .at-input, .at-textarea or .at-select; in an error field: border: 2px solid var(--at-border-danger)
## Size and spacing
- **Inside a field**: display: flex; flex-direction: column; gap: var(--at-space-2) /* 4px */
- **Between fields**: wrap fields in .at-stack; gap: var(--at-space-5) /* 16px */
## Variants
### Field
Label and control. Add a hint only when it helps.
```html
```
### Field with hint
A hint between the label and the control explains format or limits. Link it with aria-describedby.
```html
In pounds, before VAT.
```
### Field with error
Add .at-field--error and an .at-error message above the control. Set aria-invalid="true" and add the error id to aria-describedby.
```html
Enter an email address, like name@example.com
```
## When to use
- Around every text input, textarea, select and date input.
## When not to use
- Checkboxes and radios: use a fieldset with a legend instead.
## Content
- Error messages say how to fix it: "Enter a price, like 12.50", not "Invalid".
- Hints describe format or limits; they do not repeat the label.
- If a form has several errors, also show an error alert at the top.
## Do and don't
Do: The error says what to type.
```html
Enter a price, like 12.50
```
Don't: A vague error, placed after the field where it is easy to miss.
```html
Invalid input
```
## Do
- Say how to fix it: "Enter a price, like 12.50".
## Don't
- Show errors before the user has submitted.
- Only colour the border: always show the message.
## Code
```html
In pounds, before VAT.
Enter a price, like 12.50
```
## Classes
| Class | What it does |
|---|---|
| `at-field` | Wrapper |
| `at-field--error` | Error state: red, thicker border |
| `at-label` | Label |
| `at-hint` | Help text |
| `at-error` | Error message |
## Tokens
`--at-text-subtle`, `--at-text-danger`
## Accessibility
- Point aria-describedby at the hint and error; set aria-invalid="true" on error.
---
# Radio
Status: stable
Pick exactly one option from a short list.
Radios let managers pick exactly one option from a short list, with every option visible. Use them for five or fewer options. A group of radios sits in a fieldset with a legend that asks the question, and every radio in the group shares one name.
Atrium uses the browser's own radio, tinted with the accent colour. It has no separate hover style.
## Example
```html
```
## Anatomy
1. **Legend**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
2. **Radio**: width: 1.125rem; height: 1.125rem /* 18px */; accent-color: var(--at-bg-primary)
3. **Label**: font-size: var(--at-text-md) /* 16px */; the whole label is clickable
## Size and spacing
- **Radio**: 18px by 18px
- **Spacing**: radio to label: var(--at-space-3) /* 8px */; between options: var(--at-space-3) /* 8px */
## Variants
### Radio group
Pre-select the most common answer if there is a safe default. Otherwise leave all unselected and check one was chosen on save.
```html
```
| State | Specification |
|---|---|
| Unselected | browser default circle |
| Selected | dot: var(--at-bg-primary) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Disabled | greyed by the browser |
## When to use
- Exactly one choice from five or fewer options.
## When not to use
- More than about five options: use a select.
- Several choices: use checkboxes.
- A single on or off option: use a checkbox.
## Content
- Order options logically: smallest to largest, or most common first.
- Keep each label short and parallel: "Monthly", "Quarterly", "Yearly".
## Do and don't
Do: A legend that asks the question, options in order.
```html
```
Don't: No legend, and different names, so both can be selected.
```html
```
## Do
- Pre-select the most common option when it is safe to.
## Don't
- Use a single radio on its own.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-fieldset` | Groups the options |
| `at-choice` | One option |
## Tokens
`--at-bg-primary`
## Accessibility
- Give every radio in a group the same name.
---
# Select
Status: stable
Choose one option from a list of more than about five.
Selects let managers choose one option from a list. They save space when there are more than about five options. With five or fewer, radios are better because every option is visible without a click.
Atrium uses the browser's own select, so the dropdown list and its arrow look a little different in each browser and on phones. That is expected.
## Example
```html
```
## Anatomy
1. **Label**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
2. **Field**: min-height: 36px; padding: var(--at-space-3) /* 8px */; border: 1px solid var(--at-border); border-radius: var(--at-radius-1); arrow: drawn by the browser
## Size and spacing
- **Field**: min-height: 36px; width: 100% of its container
- **Type**: font-size: var(--at-text-md) /* 16px */
## Variants
### Select
Put the most common option first, or pre-select it if there is a sensible default.
```html
```
| State | Specification |
|---|---|
| Default | border: 1px solid var(--at-border) |
| Hover | border-color: var(--at-text-subtle) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Error | border: 2px solid var(--at-border-danger); plus an .at-error message |
| Disabled | opacity: 0.5; cursor: not-allowed |
## When to use
- Choosing one option from more than about five.
## When not to use
- Five or fewer options: use radios.
- Choosing several: use checkboxes.
- Hundreds of options: use a text input with search.
## Content
- Options in a sensible order: most common first, or alphabetical, or by date.
- Keep option text short so it fits on a phone.
## Do and don't
Do: Two options: show them as radios.
```html
```
Don't: A select that hides only two options.
```html
```
## Do
- Keep the native select element.
## Don't
- Build a custom dropdown.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-select` | Native select, styled |
## Tokens
`--at-border`, `--at-surface`, `--at-text`, `--at-border-bold`
## Accessibility
- Native selects work with keyboards and screen readers out of the box.
---
# Text input
Status: stable
A single line of text, with its label above.
Text inputs let managers type and edit a single line of text: a name, a reference, an amount. Every input has a visible label above it, and optional help text between the label and the field.
## Example
```html
```
## Anatomy
1. **Label**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold); color: var(--at-text)
2. **Hint (optional)**: font-size: var(--at-text-sm); color: var(--at-text-subtle)
3. **Field**: min-height: 36px; padding: var(--at-space-3) /* 8px */; border: 1px solid var(--at-border); border-radius: var(--at-radius-1); font-size: var(--at-text-md) /* 16px */
## Size and spacing
- **Field**: min-height: 36px; width: 100% of its container
- **Spacing**: label to field: var(--at-space-2) /* 4px */; between fields: var(--at-space-5) /* 16px */
## Variants
### Text input
Use the right type so phones show the right keyboard: text, email, tel, or inputmode="decimal" for money.
```html
```
| State | Specification |
|---|---|
| Default | border: 1px solid var(--at-border) |
| Hover | border-color: var(--at-text-subtle) |
| Focus | outline: 3px solid var(--at-border-focus) |
| Error | border: 2px solid var(--at-border-danger); plus an .at-error message |
| Disabled | opacity: 0.5 |
## When to use
- Short free text: names, references, amounts, email addresses.
## When not to use
- Long text: use a textarea.
- A fixed set of answers: use radios or a select.
## Content
- Labels are short nouns: "Reference", not "Please enter your reference".
- Put format help in the hint: "In pounds, before VAT".
- Do not use placeholder text: it disappears when typing and has poor contrast.
## Do and don't
Do: A visible label, with help text above the field.
```html
In pounds, before VAT.
```
Don't: Placeholder text as the label.
```html
```
## Do
- Always show a label above the input.
- Set type="email", "number" or "tel" where it fits.
## Don't
- Use placeholder text as the label.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-field` | Wraps label, input, hint and error |
| `at-input` | The input |
## Tokens
`--at-border`, `--at-surface`, `--at-text`, `--at-border-bold`
## Accessibility
- Link label and input with for / id.
---
# Textarea
Status: stable
Several lines of text.
Textareas take several lines of free text: a note to a tenant, a description of a repair. They look and behave like a text input, but are taller and can be resized up and down.
## Example
```html
```
## Anatomy
1. **Label**: font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
2. **Hint (optional)**: font-size: var(--at-text-sm); color: var(--at-text-subtle)
3. **Field**: min-height: 5rem /* 80px */; resize: vertical; padding: var(--at-space-3) /* 8px */; border: 1px solid var(--at-border); border-radius: var(--at-radius-1) /* 6px */
## Size and spacing
- **Field**: min-height: 5rem /* 80px */; width: 100% of its container; set rows="4" or more for longer text
- **Type**: font-size: var(--at-text-md) /* 16px */; stops phones zooming in on focus
## Variants
### Textarea
Set rows to roughly how much text you expect, so the box hints at the answer length.
```html
```
| State | Specification |
|---|---|
| Default | border: 1px solid var(--at-border) |
| Hover | border-color: var(--at-text-subtle) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Error | border: 2px solid var(--at-border-danger); plus an .at-error message |
| Disabled | opacity: 0.5; cursor: not-allowed |
## When to use
- Free text that may run to more than one line.
## When not to use
- A name, reference or amount: use a text input.
- A fixed set of answers: use radios or a select.
## Content
- If there is a length limit, say so in the hint: "Up to 500 characters".
- Do not use placeholder text as the label.
## Do and don't
Do: A visible label, and the limit in the hint.
```html
Up to 500 characters.
```
Don't: One row high, with a placeholder instead of a label.
```html
```
## Do
- Size it to the expected length with rows.
## Don't
- Turn off resizing.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-textarea` | Multi-line input, resizes vertically |
## Tokens
`--at-border`, `--at-surface`, `--at-text`, `--at-border-bold`
## Accessibility
- Same labelling as a text input.
---
# AI content
Status: beta
Marks text an AI drafted, so managers know to check it before it is used.
AI content marks anything an AI drafted: a reply to a tenant, a summary of a repair, a suggested rent review. It tells managers the text needs checking before it is used, and shows clearly when the AI is still writing.
It uses the discovery colour and a four-pointed star, the Atrium AI mark. Use the same treatment in every app so managers learn to spot AI output anywhere in COHO.
## Example
```html
Drafted by AI
Hi Sam, the boiler engineer is booked for Thursday between 9am and 1pm.
```
## Anatomy
1. **Label (.at-ai__label)**: star mark + "Drafted by AI"; color: var(--at-text-discovery); font-size: var(--at-text-xs) /* 12px */
2. **Content**: the AI output, editable or quoted
3. **Actions**: always a way to edit or discard; the primary action uses the output
4. **Container**: border: 1px solid var(--at-border-discovery); background: var(--at-bg-discovery-subtle); border-radius: var(--at-radius-2)
## Size and spacing
- **Padding**: block: var(--at-space-4) /* 12px */; inline: var(--at-space-5) /* 16px */
- **Label**: margin-bottom: var(--at-space-3) /* 8px */; gap star to text: var(--at-space-2) /* 4px */
## Variants
### AI content
Finished AI output, waiting for a manager to check it.
```html
Drafted by AI
3 tenancies end in the next 30 days. Two have not been offered a renewal.
```
| State | Specification |
|---|---|
| Generating | add .at-ai--generating; moving border, stops for reduced motion; aria-busy="true" |
| Ready | border: 1px solid var(--at-border-discovery) |
## When to use
- Any text, summary or suggestion an AI produced.
- While an AI is generating, to show work is happening.
## When not to use
- Content a person wrote or has already approved: remove the marking once it is used.
- Rule-based automation with no AI (a reminder sent on a schedule): that is not AI content.
## Content
- Label finished output "Drafted by AI" and in-progress output "Writing".
- Name the action that uses it: "Use reply", "Add to notes", not "Accept".
- Never present AI output as fact. If it might be wrong, say "Check before sending".
## Do and don't
Do: Marked as AI, with a way to edit before it is sent.
```html
Drafted by AI
Hi Sam, the engineer is booked for Thursday.
```
Don't: Unmarked AI text that sends in one click.
```html
Hi Sam, the engineer is booked for Thursday.
```
## Do
- Let managers edit or discard every piece of AI output.
- Keep the marking until a manager has used or approved the content.
## Don't
- Send, save or change anything with AI output without a manager pressing a button first.
- Use the discovery colour or star mark for anything that is not AI or new.
## Code
```html
Drafted by AI
Hi Sam, the boiler engineer is booked for Thursday between 9am and 1pm.
```
## Classes
| Class | What it does |
|---|---|
| `at-ai` | Container with the discovery border and background |
| `at-ai__label` | Star mark and label: "Drafted by AI" or "Writing" |
| `at-ai--generating` | Moving border while the AI is still writing |
## Tokens
`--at-border-discovery`, `--at-bg-discovery-subtle`, `--at-text-discovery`, `--at-border-primary`, `--at-text-accent-magenta`
## Accessibility
- The label is real text, so screen readers announce "Drafted by AI" before the content.
- While generating, set aria-busy="true" on the container and announce the result with a status message when it is ready.
- The moving border stops when the manager has turned on reduced motion.
---
# Empty state
Status: stable
What to show when there is nothing to show yet.
Empty states fill the space where content will appear once there is some: no readings yet, no inspections booked. They say why it is empty and, where possible, offer the action that fills it.
An empty state is not an error. If something failed to load, use an error alert instead.
## Example
```html
No meter reads yet
Reads appear here once tenants submit them.
```
## Anatomy
1. **Title**: color: var(--at-text); font-weight: var(--at-weight-bold); margin-bottom: var(--at-space-2) /* 4px */
2. **Message**: color: var(--at-text-subtle); one sentence on why it is empty
3. **Action (optional)**: usually one .at-button--primary
4. **Container**: border: 1px dashed var(--at-border); border-radius: var(--at-radius-2) /* 10px */; text-align: center
## Size and spacing
- **Padding**: block: var(--at-space-7) /* 32px */; inline: var(--at-space-5) /* 16px */
- **Width**: fills the space the content would take
## Variants
### With action
When the manager can fill it: book, request, connect.
```html
No inspections booked
Book one to remind the tenant automatically.
```
### Message only
When content arrives on its own, or a search or filter found nothing.
```html
No results for "Ashby"
Check the spelling or clear the filter.
```
## When to use
- A list or table with no rows yet.
- A search or filter with no results.
## When not to use
- Something failed to load: use an error alert.
- While loading: use a spinner or skeleton.
## Content
- Title says what is missing: "No meter reads yet".
- Message says why, or what will make content appear.
- Keep it calm and short: no jokes or illustrations needed.
## Do and don't
Do: Says what is missing and when it will appear.
```html
No meter reads yet
Reads appear here once tenants submit them.
```
Don't: Sounds like an error and does not help.
```html
Oops!
Nothing to see here.
```
## Do
- Explain why it is empty and offer the next step.
## Don't
- Show a blank table with headers only.
## Code
```html
No meter reads yet
Reads appear here once tenants submit them.
```
## Classes
| Class | What it does |
|---|---|
| `at-empty` | Dashed, centred box |
| `at-empty__title` | Bold first line |
## Tokens
`--at-border`, `--at-text-subtle`, `--at-text`
## Accessibility
- Plain text, no images needed.
---
# Inline alert
Status: stable
A message about the whole page or a section: information, success, a warning or an error.
Alerts tell managers about something on the whole screen or a section: a save worked, something needs checking, or something went wrong. They sit at the top of the area they are about.
## Example
```html
Settings savedReminders will go out at 9am.
```
## Anatomy
1. **Edge**: border-left: 4px solid (status colour)
2. **Title**: font-weight: var(--at-weight-bold); display: block
3. **Message**: font-size: var(--at-text-sm); background: status -bg token; padding: var(--at-space-4) var(--at-space-5)
## Variants
### Information
Neutral news: something is syncing, a change is coming.
```html
SyncingReadings update every hour.
```
### Success
Confirms an action worked. Show it after saving.
```html
SavedYour settings are saved.
```
### Warning
Something needs attention soon, but nothing has failed.
```html
Check the datesTwo tenancies end this week.
```
### Error
Something failed. Say what happened and what to do next.
```html
Could not saveCOHO did not respond. Try again.
```
### Discovery
Something new worth knowing: a new feature, or a first-use tip. Show it once, and let managers dismiss it.
```html
New: bulk remindersSelect several tenants to remind them all at once.
```
## When to use
- After saving.
- When something on the page needs attention.
- At the top of a form that failed validation.
## When not to use
- For a single field error: use the field error.
- For marketing or tips.
## Content
- Lead with a short title: "Saved", "Could not save".
- Then one sentence on what to do next.
## Do and don't
Do: Say what failed and how to fix it.
```html
Could not send reminders3 tenants have no email address. Add them and try again.
```
Don't: Codes and vague messages.
```html
Error 500Something went wrong.
```
## Do
- Say what happened and what to do next.
## Don't
- Stack several alerts.
## Code
```html
SyncingReadings update every hour.
SavedYour settings are saved.
Check the datesTwo tenancies end this week.
Could not saveCOHO did not respond. Try again.
```
## Classes
| Class | What it does |
|---|---|
| `at-alert` | Neutral |
| `at-alert--info / --success / --warning / --danger` | Variants |
| `at-alert__title` | Bold first line |
| `at-alert--success` | Success |
| `at-alert--warning` | Warning |
| `at-alert--danger` | Error |
| `at-alert--discovery` | Something new |
## Tokens
`--at-border`, `--at-bg-neutral`, `--at-border-info`, `--at-bg-info-subtle`, `--at-border-success`, `--at-bg-success-subtle`, `--at-border-warning`, `--at-bg-warning-subtle`, `--at-border-danger`, `--at-bg-danger-subtle`, `--at-border-discovery`, `--at-bg-discovery-subtle`
## Accessibility
- Use role="alert" for errors and role="status" for confirmations.
---
# Spinner and skeleton
Status: stable
Show that something is loading.
Loading indicators show that something is on its way. Use a spinner for a short wait or an action in progress. Use skeleton lines when you know the shape of what is coming, such as rows of a list, so the page does not jump when it loads.
Both stop moving for people who have turned on reduced motion.
## Example
```html
Loading readings
```
## Anatomy
1. **Spinner (.at-spinner)**: 20px by 20px; border: 2px solid var(--at-border); border-top-color: var(--at-border-primary); animation: 0.8s spin
2. **Skeleton line (.at-skeleton)**: height: 1rem /* 16px */; background: var(--at-bg-neutral); border-radius: var(--at-radius-1); animation: 1.4s pulse
## Size and spacing
- **Skeleton widths**: .at-skeleton: 100%; .at-skeleton--medium: 70%; .at-skeleton--short: 40%
- **Spinner**: width and height: 1.25rem /* 20px */
## Variants
### Spinner
A short wait, or an action in progress. Put a few words beside it.
```html
Loading readings
```
### Skeleton
Placeholder lines in the shape of the content that is coming. Mix widths so it looks like text.
```html
```
## When to use
- Spinner: waiting for a short load or a save.
- Skeleton: the first load of a list, table or card.
## When not to use
- Loads that finish in under about 300ms: show nothing, to avoid a flash.
- Nothing to load: use an empty state.
## Content
- Say what is loading: "Loading readings", not just "Loading".
## Do and don't
Do: One spinner with a short label.
```html
Loading readings
```
Don't: Several spinners and no label.
```html
```
## Do
- Keep the layout stable while loading.
## Don't
- Show a spinner for the whole screen.
## Code
```html
Loading readings
```
## Classes
| Class | What it does |
|---|---|
| `at-spinner` | Rotating ring |
| `at-skeleton` | Grey placeholder bar, full width |
| `at-skeleton--short / --medium` | 40% / 70% width |
## Tokens
`--at-border`, `--at-border-primary`, `--at-bg-neutral`
## Accessibility
- Give the spinner role="status" and a label. Animations stop when reduced motion is on.
---
# Status tag
Status: stable
A short status label: Paid, Overdue, Draft.
Tags are short status labels: Paid, Overdue, Draft. They let managers scan a list for what needs attention. Always use a word, not colour alone, so the status is clear to everyone.
## Example
```html
DraftScheduledPaidDue soonOverdue
```
## Anatomy
1. **Label**: font-size: var(--at-text-xs) /* 12px */; font-weight: var(--at-weight-bold); line-height: 1.4; color: status colour
2. **Container**: border: 1px solid currentColor; border-radius: var(--at-radius-1) /* 6px */; background: status -bg token
## Size and spacing
- **Padding**: block: var(--at-space-1) /* 2px */; inline: var(--at-space-3) /* 8px */
- **Spacing**: between tags: use .at-inline (8px gap)
## Variants
### Neutral
No particular status: Draft, Archived, Not started.
```html
Draft
```
### Information
Something planned or in progress: Scheduled, Syncing.
```html
Scheduled
```
### Success
Done and fine: Paid, Active, Connected.
```html
Paid
```
### Warning
Needs attention soon: Due soon, Expiring.
```html
Due soon
```
### Danger
Needs attention now: Overdue, Failed.
```html
Overdue
```
### Discovery
Something new: New, Beta, AI drafted.
```html
New
```
### Bold (emphasis: High)
Add at-tag--bold for a solid fill. Use it for the one status that most needs attention in a view, such as Overdue in a list of otherwise subtle tags.
```html
OverdueDue todayNew
```
### Accent (emphasis: Low)
Categories with no status meaning, such as property types. Six hues: blue, teal, green, orange, purple and magenta. If swapping two hues would change what the tag means, use a status tag instead.
```html
HMOStudioFlatStudent
```
## When to use
- Showing the status of a record in a table, summary list or card.
## When not to use
- Actions: use a button.
- Long text: tags are one or two words.
- More than one bold tag per view: bold only works when it is rare.
## Content
- One or two words, sentence case: "Due soon", not "DUE SOON".
- Use the same word for the same status on every screen.
## Do and don't
Do: Short words that carry the meaning on their own.
```html
OverduePaid
```
Don't: A symbol only, and a long label in capitals.
```html
!PAYMENT RECEIVED IN FULL
```
## Do
- Use one or two words.
## Don't
- Rely on colour alone: the word must say the status.
## Code
```html
DraftScheduledPaidDue soonOverdue
```
## Classes
| Class | What it does |
|---|---|
| `at-tag` | Neutral |
| `at-tag--info` | Information |
| `at-tag--success` | Good |
| `at-tag--warning` | Needs attention |
| `at-tag--danger` | Problem |
| `at-tag--discovery` | New, beta, AI |
| `at-tag--bold` | Solid fill, combine with a status |
| `at-tag--blue / --teal / --green / --orange / --purple / --magenta` | Accent hues, no meaning |
## Tokens
`--at-text-subtle`, `--at-text-info`, `--at-bg-info-subtle`, `--at-text-success`, `--at-bg-success-subtle`, `--at-text-warning`, `--at-bg-warning-subtle`, `--at-text-danger`, `--at-bg-danger-subtle`, `--at-text-discovery`, `--at-bg-discovery-subtle`, `--at-text-inverse`, `--at-bg-danger-bold`, `--at-bg-accent-blue`, `--at-text-accent-blue`
## Accessibility
- The text carries the meaning, so colour-blind users get the same information.
---
# Summary list
Status: stable
Key and value pairs for one record, such as a property or tenancy.
Summary lists show the details of one record as key and value pairs: a property, a tenancy, a payment. Keys sit in a narrow column on the left and values on the right, with a line between rows.
Use a summary list for one thing. For many things with the same fields, use a table.
## Example
```html
Property
Flat 3, 22 Ashby Road
Tenancy
1 Sep 2026 to 31 Aug 2027
Rent
£650.00 per month
```
## Anatomy
1. **Key (dt)**: color: var(--at-text-subtle); font-weight: var(--at-weight-bold); padding-right: var(--at-space-4) /* 12px */
2. **Value (dd)**: color: var(--at-text); font-size: var(--at-text-sm) /* 14px */
3. **Row divider**: border-bottom: 1px solid var(--at-border); padding: var(--at-space-3) 0 /* 8px */
## Size and spacing
- **Columns**: grid-template-columns: minmax(8rem, 1fr) 2fr; key column at least 128px
- **Rows**: padding: var(--at-space-3) 0 /* 8px */
## Variants
### Summary list
Plain values. Put tags or links in the value column when they help.
```html
Status
Active
Supplier
Octopus Energy
Last reading
4 Oct 2026
```
### In a card
Give the record a title by putting the list in a card.
```html
Tenancy
Start
1 Sep 2026
End
31 Aug 2027
```
## When to use
- Showing the details of one record.
- A check-your-answers page before saving.
## When not to use
- Many records: use a table.
- Editable values: use fields in a form.
## Content
- Keys are short nouns: "Rent", "Start date".
- Show an empty value as "Not set", not a blank or a dash.
- Dates as "4 Oct 2026"; money as "£650.00".
## Do and don't
Do: Formatted money, and "Not set" for a missing value.
```html
Rent
£650.00 per month
Deposit
Not set
```
Don't: Database names as keys, and a blank value.
```html
rent_amount
650
deposit_amount
```
## Do
- Keep keys short.
## Don't
- Put form inputs in it.
## Code
```html
Property
Flat 3, 22 Ashby Road
Tenancy
1 Sep 2026 to 31 Aug 2027
Rent
£650.00 per month
```
## Classes
| Class | What it does |
|---|---|
| `at-summary` | Two-column definition list |
## Tokens
`--at-border`, `--at-text-subtle`
## Accessibility
- A dl with dt / dd reads as name and value pairs.
---
# Table
Status: stable
Rows of data the user compares or scans.
Tables show rows of records managers compare and scan: properties, tenancies, payments. Keep columns few, especially in narrow quick views, and right-align money and numbers.
## Example
```html
Property
Tenant
Due
22 Ashby Road
Sam Lee
£650.00
9 Mill Lane
Ana Ruiz
£720.00
```
## Anatomy
1. **Header cell**: background: var(--at-bg-neutral); color: var(--at-text-subtle); font-weight: var(--at-weight-bold)
2. **Cell**: padding: var(--at-space-3) var(--at-space-4); border-bottom: 1px solid var(--at-border)
3. **Number cell (.at-num)**: text-align: right; font-variant-numeric: tabular-nums
## When to use
- Several records with the same fields.
## When not to use
- One record: use a summary list.
- Page layout.
## Content
- Short column headings.
- Dates as "4 Oct 2026"; money as "£650.00".
## Do and don't
Do: Status in words, money right-aligned.
```html
Tenant
Status
Due
Sam Lee
Overdue
£650.00
```
Don't: Status shown only by colour; unformatted money.
```html
Tenant
Due
Sam Lee
650
```
## Do
- Right-align money and numbers.
- Put row actions in the last column as link buttons.
## Don't
- Use zebra stripes or coloured rows.
## Code
```html
Property
Tenant
Due
22 Ashby Road
Sam Lee
£650.00
9 Mill Lane
Ana Ruiz
£720.00
```
## Classes
| Class | What it does |
|---|---|
| `at-table` | Full-width table |
| `at-num` | Right-aligned, tabular numbers |
## Tokens
`--at-border`, `--at-text-subtle`, `--at-bg-neutral`
## Accessibility
- Use th for headers; add a caption when the table has no visible heading.
---
# Card
Status: stable
A bordered box that groups related content, such as one tenancy or one setting group.
Cards group related content into one bordered box: one tenancy, one inspection, one group of settings. They help managers see where one thing ends and the next begins.
Use cards sparingly. A page made only of cards is harder to scan than a table or a summary list.
## Example
```html
Next inspection
14 Oct 2026, Flat 3, 22 Ashby Road
```
## Anatomy
1. **Title (optional)**: font-size: var(--at-text-md) /* 16px */; font-weight: var(--at-weight-bold); margin-bottom: var(--at-space-3) /* 8px */
2. **Content**: any content: text, a summary list, buttons
3. **Container**: border: 1px solid var(--at-border); border-radius: var(--at-radius-2) /* 10px */; background: var(--at-surface); box-shadow: var(--at-shadow-raised)
## Size and spacing
- **Padding**: padding: var(--at-space-5) /* 16px */
- **Width**: fills its container; space cards with .at-stack (16px gap)
## Variants
### Card (emphasis: Medium)
The default: white with a light shadow, for a record or a group of settings.
```html
Reminders
Sent 3 days before rent is due.
```
### Muted card (emphasis: Low)
Grey background and no shadow, for supporting content beside the main card, such as a tip or a summary.
```html
How this works
Readings sync from the supplier every hour.
```
## When to use
- Grouping the details of one thing.
- Separating groups of settings on one page.
## When not to use
- Several records with the same fields: use a table.
- Wrapping the whole page: COHO already draws a frame.
- Inside another card.
## Content
- Card titles are short nouns: "Next inspection", "Reminders".
- Put at most one or two actions in a card.
## Do and don't
Do: Separate cards for separate groups, spaced with a stack.
```html
Reminders
Sent 3 days before rent is due.
Late fees
Off
```
Don't: Cards inside cards.
```html
Reminders
Sent 3 days before rent is due.
```
## Do
- Give each card a heading.
## Don't
- Nest cards.
- Use coloured card backgrounds.
## Code
```html
Next inspection
14 Oct 2026, Flat 3, 22 Ashby Road
```
## Classes
| Class | What it does |
|---|---|
| `at-card` | White box, border, small shadow |
| `at-card--muted` | Grey surface, no shadow |
| `at-card__title` | Card heading |
## Tokens
`--at-border`, `--at-surface-raised`, `--at-shadow-raised`, `--at-bg-neutral`
## Accessibility
- Use a real heading element for the card title so it appears in the page outline.
---
# Page header
Status: stable
The top of every app screen: the page title, an optional subtitle and the main action.
The page header sits at the top of every quick view and full app page. It says what the screen is and holds the one main action for the whole screen.
COHO draws the window frame, title bar and close control around your app. The page header is the first thing inside that frame, so do not repeat the app name or add your own close button.
## Example
```html
Meter reads
12 properties due this month
```
## Anatomy
1. **Title (h1)**: font-size: var(--at-text-xl) /* 24px */; font-weight: var(--at-weight-heavy) /* 700 */; letter-spacing: -0.01em
2. **Subtitle (optional)**: font-size: var(--at-text-sm) /* 14px */; color: var(--at-text-subtle)
3. **Main action (optional)**: one .at-button--primary; or a secondary button if the screen has no main action
4. **Divider**: border-bottom: 1px solid var(--at-border)
## Size and spacing
- **Layout**: display: flex; align-items: center; justify-content: space-between
- **Spacing**: gap: var(--at-space-4) /* 12px */; padding-bottom: var(--at-space-4) /* 12px */; margin-bottom: var(--at-space-5) /* 16px */
## Variants
### Title and action
The usual header: what the screen is, and the main thing to do there.
```html
Inspections
```
### Title and subtitle
Add a subtitle for a count or a short status. Leave out the action when the screen is for reading only.
```html
Energy usage
Updated 5 minutes ago
```
## When to use
- At the top of every quick view and full app page.
- To hold the one main action for the whole screen.
## When not to use
- For section headings inside a page: use h2 or h3.
- For more than two actions: move the rest into the page.
## Content
- Name the screen, not the app: "Meter reads", not "Rent Radar".
- Keep the subtitle to one short line: a count, a date or a status.
- Use sentence case.
## Do and don't
Do: Says what the screen is, with one main action.
```html
Meter reads
12 properties due this month
```
Don't: A marketing title, two primary buttons and a home-made close button.
```html
Welcome to Meter Master Pro!
```
## Do
- Use one h1 per screen.
- Keep the title to what the screen is, e.g. "Meter reads".
## Don't
- Add your own close button: COHO draws the frame and its close control.
- Put your logo here at full size; a small mark beside the title is fine.
## Code
```html
Meter reads
12 properties due this month
```
## Classes
| Class | What it does |
|---|---|
| `at-header` | Flex row with a bottom border |
| `at-header__title` | Page title (h1) |
| `at-header__subtitle` | Muted line under the title |
## Tokens
`--at-border`, `--at-text-subtle`
## Accessibility
- The title is the page h1, so screen readers announce it first.
---
# Stack and inline
Status: stable
Two layout helpers that space things consistently: stack (vertical) and inline (horizontal, wrapping).
Two helpers space things consistently so you do not need your own margins. Stack puts items in a column with even gaps. Inline puts items in a row and wraps them onto the next line when space runs out, which matters in narrow quick views.
## Example
```html
```
### Tight stack
Closely related items, such as lines of a loading skeleton or a short list. 8px between items.
```html
Item
Item
```
### Inline, end
Buttons at the end of a form or card, main action last.
```html
```
### Inline, between
Two things pushed to opposite ends, such as a label and a value or a count and an action.
```html
3 selected
```
## When to use
- Spacing any group of items: use these instead of margins.
- Rows of buttons or tags that must wrap in narrow views.
## When not to use
- Rows of data with columns: use a table.
- Key and value pairs: use a summary list.
## Do and don't
Do: Inline handles the gap and wraps on small screens.
```html
```
Don't: Hand-set margins that do not match the scale.
```html
```
## Do
- Put the primary button last in a button row.
## Don't
- Mix your own margins with stack gaps.
## Code
```html
First
Second
```
## Classes
| Class | What it does |
|---|---|
| `at-stack` | Column with 16px gaps |
| `at-stack--tight` | Column with 8px gaps |
| `at-inline` | Wrapping row with 8px gaps |
| `at-inline--end` | Right-aligned row |
| `at-inline--between` | Space between items |
## Tokens
## Accessibility
- Keep DOM order the same as visual order.
---
# Pagination
Status: stable
Move through a long list a page at a time.
Pagination moves through a long list a page at a time. Atrium keeps it simple: Previous, the current page, and Next. Put it under the table or list it controls.
The wrapper only handles layout. Previous and Next are ordinary secondary buttons, so they get the button hover, focus and disabled styles.
Pagination is the one place Atrium disables buttons: Previous on the first page and Next on the last, where there is genuinely nowhere to go. Everywhere else, keep buttons pressable.
## Example
```html
```
## Anatomy
1. **Previous and Next**: .at-button (secondary); disabled on the first and last page
2. **Page status**: font-size: var(--at-text-sm) /* 14px */; color: var(--at-text-subtle)
## Size and spacing
- **Layout**: display: flex; justify-content: space-between; align-items: center
- **Spacing**: gap: var(--at-space-4) /* 12px */
## Variants
### Pagination
Disable Previous on the first page and Next on the last, rather than hiding them, so the layout does not jump.
```html
```
| State | Specification |
|---|---|
| First page | Previous disabled |
| Middle page | both enabled |
| Last page | Next disabled |
## When to use
- Lists longer than about 25 rows.
## When not to use
- Short lists: show everything.
- Quick views with only a few rows: link to the full app page instead.
## Content
- Status as "Page 2 of 4".
- Buttons say "Previous" and "Next", not arrows alone.
## Do and don't
Do: Words on the buttons, and the current page in plain text.
```html
```
Don't: Arrow-only buttons and no page total.
```html
```
## Do
- Show where the user is.
## Don't
- Use page numbers for very long lists.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-pagination` | Previous / position / Next |
## Tokens
`--at-text-subtle`
## Accessibility
- Wrap in a nav with an aria-label.
---
# Tabs
Status: stable
Switch between views of the same thing.
Tabs switch between views of the same thing without leaving the screen: Due, Submitted and History readings, for example. The selected tab has an accent underline.
Tabs are buttons with role="tab" inside a role="tablist". Move between them with the arrow keys, and show only the selected panel.
## Example
```html
```
## Anatomy
1. **Selected tab**: color: var(--at-text); border-bottom: 2px solid var(--at-border-primary)
2. **Tab**: color: var(--at-text-subtle); font-size: var(--at-text-sm) /* 14px */; font-weight: var(--at-weight-bold)
3. **Tab list**: border-bottom: 1px solid var(--at-border); margin-bottom: var(--at-space-5) /* 16px */
## Size and spacing
- **Tab**: padding: var(--at-space-3) var(--at-space-4) /* 8px 12px */
- **Spacing**: gap between tabs: var(--at-space-2) /* 4px */
## Variants
### Tabs
Two to five tabs. Put the most used view first and select it by default.
```html
```
| State | Specification |
|---|---|
| Default | color: var(--at-text-subtle) |
| Hover | color: var(--at-text) |
| Focus | outline: 3px solid var(--at-border-focus); outline-offset: 2px |
| Selected | color: var(--at-text); border-bottom-color: var(--at-border-primary) |
## When to use
- Two to five views of the same thing.
## When not to use
- Steps in a process: show one step per page.
- Going to a different part of the app: use links.
- More than five views: use a select.
## Content
- One or two words per tab: "Due", "History".
- Do not put counts in every tab; add one only where it helps, such as "Due (3)".
## Do and don't
Do: Short labels for views of the same readings.
```html
```
Don't: Tabs used as steps in a form.
```html
```
## Do
- Keep labels to one or two words.
## Don't
- Nest tabs.
## Code
```html
Due readings
```
## Classes
| Class | What it does |
|---|---|
| `at-tabs` | Tab row |
| `at-tab` | One tab; aria-selected="true" marks the current one |
## Tokens
`--at-border`, `--at-text-subtle`, `--at-text`, `--at-border-primary`
## Accessibility
- Use role="tablist" / "tab" / "tabpanel" and move with arrow keys.
---
# Dialog and confirm
Status: stable
Ask the user to confirm before something that cannot be undone.
Dialogs interrupt to ask for a decision, usually to confirm something that cannot be undone. Use the native dialog element opened with showModal(), which handles focus and the Escape key for you.
## Example
```html
```
## Anatomy
1. **Title**: h2, phrased as the question
2. **Body**: one or two sentences on what will happen
3. **Actions**: right-aligned; cancel first, then the action; action named for what it does
## Size and spacing
- **Container**: max-width: 28rem /* 448px */; padding: var(--at-space-6) /* 24px */; border-radius: var(--at-radius-2) /* 10px */
- **Actions**: margin-top: var(--at-space-5); gap: var(--at-space-3)
## When to use
- Confirming a destructive or irreversible action.
## When not to use
- Forms or long content: use a page.
- Information only: use an alert.
- Inside another dialog.
## Content
- Title as a question: "Disconnect Rent Radar?"
- Buttons say the action: "Disconnect", not "OK".
## Do and don't
Do: A question, and an action button that names the action.
```html
```
Don't: Vague title and Yes / No buttons.
```html
```
## Do
- Title it as the question.
- Name the action on the button: "Disconnect", not "OK".
## Don't
- Open a dialog from a dialog.
## Code
```html
```
## Classes
| Class | What it does |
|---|---|
| `at-dialog` | Use on a native dialog element; opened with showModal() |
| `at-dialog__actions` | Right-aligned buttons |
## Tokens
`--at-border`, `--at-surface-overlay`, `--at-text`, `--at-shadow-overlay`
## Accessibility
- A native dialog with showModal() traps focus and closes on Escape.
---
# Settings page
A page where a manager configures the app.
One card per group of settings, one primary Save at the bottom. Show a success alert at the top after saving. If your app uses COHO light config instead, COHO draws this form for you.
```html
Reminder settings
```
---
# List and detail
A table of records, and a detail view for one of them.
### List
```html
Inspections
Property
Status
22 Ashby Road
Due soon
9 Mill Lane
Done
```
### Detail
```html
22 Ashby Road
Inspection
Due
14 Oct 2026
Inspector
Jo Patel
Status
Due soon
```
In a quick view, open the detail in place and offer a link back. Do not open new windows.
---
# Form submit and validation
Check fields when the form is submitted, then show what to fix.
Validate on submit, not while the user types. Show a summary alert at the top and the message beside each field. Move focus to the summary, keep what the user typed, and never disable the submit button to block submission.
```html
Check 1 fieldEnter a monthly price.
```
---
# AI moments
How an app shows AI at work, and hands its output to a manager to check.
Many Atrium apps use AI to draft replies, summarise records or suggest next steps. Managers stay in charge: the AI drafts, the manager decides. Every AI moment follows the same three steps, so managers recognise them in any app.
| Step | What the manager sees |
|---|---|
| 1. Ask | A button that names what the AI will do: "Draft reply", "Summarise". Not "Ask AI" or "Magic". |
| 2. Generating | An [AI content](../../components/ai-content/) block with the moving border and the label "Writing". Keep the rest of the screen usable. |
| 3. Check and use | The output marked "Drafted by AI", with Edit and a primary action that names the result: "Use reply". Nothing is sent or saved until the manager presses it. |
### Generating
```html
Writing
```
### Ready to check
```html
Drafted by AI
Hi Sam, the boiler engineer is booked for Thursday between 9am and 1pm. They will call 30 minutes before arriving.
```
## Rules
- **A person presses the button.** AI output is never sent to a tenant, saved to a record or used to change money without a manager choosing to.
- **Always marked.** Keep the AI content marking until the manager has used or approved the output.
- **Always editable.** Every output has Edit and Discard.
- **Honest when unsure.** If the AI could be wrong, say so: "Check the dates before sending."
- **Errors in plain words.** "Could not draft a reply. Try again, or write one yourself." Never show model or API errors.