Swiss Moss

Paper, ink and one green.

A small, opinionated design system for calm interfaces: a token set, a CSS component library, and a few dependency-free behaviours. Light and dark from the same tokens. No build step.

One sentence first

Every screen opens with a headline that states the situation in plain words. Counts live inside sentences, never as big numbers with tiny labels.

Lines, not boxes

Hairlines separate content. Cards are for things that float or take input: a command bar, a reply box, a dialog, a toast.

Nothing moves until you do

Motion answers an action and nothing else. Reduced-motion preferences are respected everywhere.

Install

Copy tokens/, css/ and js/ into your project, or point at them from this repository. Load Inter and Inter Tight from Google Fonts (both are open fonts), then:

<link rel="stylesheet" href="css/swiss-moss.css">
<script src="js/swiss-moss.js" defer></script>

Every class is prefixed with sm-. Every colour, size and radius is a custom property prefixed --sm-, so your own CSS can use them too. Building with an agent? Point it at DESIGN.md: tokens, rules and a component index on one page. Designing by hand? Open design/swiss-moss.figcraft in figCraft. Dark mode follows the system by default; add data-theme="dark" or "light" on <html> to force one, or use the buttons above, which persist the choice.

Tokens

Switch the theme above to see the second set. Both live in tokens/tokens.css, and tokens/tokens.json holds the same values for tooling. line is the hairline for dividers and cards; control-line is the stronger border for inputs, selects, checkboxes, switch tracks and range tracks, at 3:1 against the page so a control is visible without its fill. npm run check verifies every text and control pair in both themes.

Colour

Spacing, on a 28px rhythm

Radius and shadow

s 8
m 12
l 16
pill
rest
lift

Typography

Inter Tight for display and item titles, Inter for everything else. Sentence case everywhere; no all-caps labels.

Display xl

Display l, the screen headline.

Display m for secondary heroes

Title, 28px

Heading, 20px

Item, 19px: the title of a row, the most read style

Lead, 17px, for an introduction under a headline.

Body, 15px. The default. Line height 1.55 so paragraphs breathe.

Small, 14px, for secondary lines.

Caption, 13px, muted, for who and when.

Key, 12px, for tiny labels and keyboard hints

<h1 class="sm-display-l">Seven things need you.</h1>
<p class="sm-item">Rotate the production keys before the audit</p>
<p class="sm-caption">Priya, 12 min</p>

Buttons

Pills only. One primary per view. Ghost for secondary choices, quiet for low-stakes escapes, signal for irreversible actions, danger for destructive ones.


      

Sizes, icons and groups


    

Chips, tags, badges, labels

Chips are interactive: filters you toggle or values you remove. Tags are static descriptors. Badges are counts. Labels name fields.

Chips: toggle, removable, with avatar

priya@example.com LOLeah Okafor not-an-email small chip

      

Tags

Default Approved Overdue Blocked In review Outline Live Degraded

      

Badges and labels

7 128 3 Inbox 12 K

    

Inputs

Every native type, styled once. Fields carry their label, hint and error; add data-sm-validate to a field to show the browser's own validity on blur.

Please enter a name.
We only use it to sign you in.That does not look like an email address.
K
EURper month

    

Tag inputs

A field that turns what you type into chips: email recipients, labels, keywords, numbers. Enter, comma, paste and blur all commit a value; Backspace removes the last chip; invalid values are kept but marked so people can fix them. Add data-sm-suggest for a typeahead list and single for exactly one value.

priya@example.comtomas@example.com
Type an address and press Enter, comma or space.
platform
443

      

Read the values with element.getValues() or from the hidden input (comma separated); set them with element.setValues([...]); listen for sm:change.

Selection controls


    

Feedback

Alerts

Saved. Your settings follow you to every device.
Heads up. Slack returns at most 20 messages per search.
Token expires in 2 days. Reconnect Jira to keep syncing.
Could not save. The database refused the write: permission denied.
Neutral note without an icon.

      

Toast, dialogs, tooltip

Revoke all tokens?

Every connected person will need to sign in to their tools again. This cannot be undone.

13:58Starts in 2 minutes

Platform weekly sync

14:00 to 14:45 with Priya, Tomas, Leah and 5 others.


      

Progress, spinner, skeleton, empty state

Reading your inbox
Nothing needs you right now.

New items land here as they arrive. Everything else waits quietly in the Feed.


    

Data display

Cards and stats

Plain card

For things that float or take input.

Tinted card

For the thing being acted on, or a note.

Lifted card

For something being dragged or a popover.

48people connected+6 this week
1,204unwraps in 24 hwithin the usual range
3failed syncsneeds a look

      

List rows, the queue pattern

  • Rotate the production KMS keys before the audit windowPLAT-412, Priya Nair
    12 min
  • Checkout latency spike, root cause still open#incident-1002, Tomas Weber
    25 min
  • Observability contract renewal: counter-offer attachedEmail, Mara Lindqvist
    1 h

      

Table, avatars, accordion

NameGroupConnectedLast sync
AFAna Ferreiraplatform-adminsGoogle Slack Jira2 min ago
PNPriya NairplatformGoogle Slack6 min ago
LOLeah OkaforplatformJira expired1 h ago
AFPN
AFPNTW+5
What happens to the original email when I mark it done?
By default the thread is marked read. Archiving is a separate switch.
Can I undo?
The item returns to its pile. A thread already marked read stays read.

    

More inputs

Steppers, one-time codes, passwords, money, phone numbers, dates and multi-select. Each is plain HTML first; the behaviour is opt-in through a data-sm-* attribute.

Number stepper and one-time code

Code from your phone

      

Password with reveal and strength

Twelve characters or more, mixed case, a digit and a symbol.

      

Money and phone

EUR
Type 1234.5 and leave the field.

      

Calendar, date picker and date range


      

Multi-select: a menu of checkbox items, the badge counts the choices


    

Overlays

Popovers use the native popover attribute, so opening, closing and light dismiss need no script; Swiss Moss only places them under their button. Sheets, drawers and the palette are <dialog> elements and open with data-sm-dialog-open.

Popover and info tip

Anyone with the link can view.

Links expire after 30 days.

Seat count

      

Sheet, drawer, and a dialog that becomes a drawer on phones

Filters

Archive 3 threads?

They leave the queue and come back if someone replies.


      

Context menu: right-click the area, or focus it and press Shift+F10

Right-click here

      

Command palette: press Cmd+K or Ctrl+K

  • Inbox G I
  • Settings G S
  • Switch theme
  • New queue

    

Page structure

Top bar, banner and page header

Queue
DS
  1. Projects
  2. Ingestion worker

Ingestion worker

Reads mailboxes every five minutes.

Page content.


      

Labeled divider, carousel, aspect ratio and scroll area

or continue with email
4 / 3

A scroll area keeps a long list inside a fixed height with a thin scrollbar in the control colour.

Use it for logs, side lists and pickers.

It never hides the scrollbar, so people can see there is more.


    

Tables and records

Data table: toolbar, sortable columns, row selection and a bulk action bar

Priya ShahActive122 Mar 2025
Tomas BergInvited314 Sep 2026
Leah OkaforSuspended030 Nov 2024
Ana RuizActive278 Jan 2026

      

KPI with delta and sparkline, bar list, status

Open threads42↓ Down 12%vs last week
Failed syncs7↑ Up 3→ Flat
  • Slack82%
  • Jira54%
  • Google Workspace31%
HealthyDegradedDownMaintenanceUnknown

      

Description list and timeline

Owner
Platform team
Schedule
Every five minutes
Token
sk_live_7f3a…
  1. Run finished. 214 messages read.
  2. Slack slowed down. Retried twice.
  3. Run started.

      

Code block, truncation and an error state

shell
npm installnpm run check
ingestion-worker-production-eu-central-1-blue.internal

Long notes stay at two lines until someone asks for more. The rest of the text is still in the page, so search and screen readers find it, and the button below opens the block in place.

Could not load runs.

The worker did not answer in 10 seconds.


    

Writing

  • Headlines are full sentences with a full stop: "Four replies, one sitting."
  • Buttons say what happens: "Approve in Jira", "Send and next", "Revoke all tokens".
  • Hints explain consequences in one line: "Dismissing silences all your devices."
  • Sentence case everywhere. No exclamation marks, no emoji in the interface.
  • Signal colour means something is wrong or irreversible. It never decorates.