1. HTML first
A <button> is already a Fernand button. An <input type="submit"> too. A <dialog> is a modal, an <article> a card, <details> an accordion.
Fernand CSS is the design language of the Fernand app, packaged as a small, classless-first framework. Write plain HTML — buttons, forms, tables, dialogs look right out of the box. Add a class only when you want to change something.
One stylesheet, one optional script. No build step.
Copy the css/, fonts/ and (optionally) js/ folders, then link them:
| File | Role |
|---|---|
css/fernand.css | The framework. Tokens, base, forms, components, utilities — in a single @layer fernand. |
js/fernand.js | Optional, ~8 KB unminified. ARIA tabs, theme switcher, Fernand.toast(), fallbacks for command and closedby. |
fonts/ | Inter, the same files as the app. |
Four ideas you need to know before writing any markup.
A <button> is already a Fernand button. An <input type="submit"> too. A <dialog> is a modal, an <article> a card, <details> an accordion.
Use a class only to deviate from the default: .secondary, .ghost, .small, .boxed… ARIA attributes describe states: aria-invalid, aria-pressed, aria-busy, aria-current.
.info, .success, .warning and .danger recolor any component: buttons, alerts, badges, tags, toasts, progress bars, table rows…
Every color, size, radius, shadow and duration is a CSS variable. Everything is in a cascade layer, so your own CSS always wins — no !important.
Follows the operating system by default. Try the switcher at the top of the menu.
Every color is declared once with light-dark(), so a component never needs a dark-mode override. The active scheme is driven by the CSS color-scheme property:
| You write | Result |
|---|---|
| nothing | Follows the OS (prefers-color-scheme) |
<html data-theme="dark"> | Forces dark mode on the whole page |
<html data-theme="light"> | Forces light mode |
<body class="dark-mode"> | Same as data-theme="dark" — the class the app already uses |
<section data-theme="dark"> | Works on any element: only that subtree turns dark |
data-theme="light"
data-theme="dark"
With fernand.js, any element with data-theme-set switches the theme and remembers it in localStorage. aria-pressed is kept in sync for you.
Or from JavaScript: Fernand.setTheme('dark'). While switching, transitions are disabled for one frame (data-theme-switching) so the whole UI swaps at once instead of fading for 750 ms.
.only-light / .only-dark
Use light-dark() for any custom color and it will follow the theme automatically:
Change a token once, every component follows. Swatches below are live: switch the theme to see dark values.
These keep the app's historical names (--white is the background, so it is dark in dark mode). Prefer the semantic tokens below in new code.
| Variable | Variable | ||
|---|---|---|---|
--white | --black | ||
--grey-light | --grey-medium | ||
--grey-strong | --border / --border-strong | ||
--blue | --blue-pale | ||
--green | --green-pale | ||
--orange | --orange-pale | ||
--red | --red-pale |
| Variable | Used for | |
|---|---|---|
--color-bg | Page background, dialogs | |
--color-bg-subtle | Side areas, card & dialog footers, table heads | |
--color-surface | Cards, tags, accordions | |
--color-surface-raised | Popovers, menus, tooltips | |
--color-field · --color-field-border · --color-field-disabled | Inputs, selects, textareas | |
--color-text | Body text, titles | |
--color-text-muted | Labels, descriptions, secondary text | |
--color-text-subtle | Section labels (h2), placeholders, icons | |
--color-border · --color-border-strong | Separators, outlines | |
--color-kbd | Keyboard hints | |
--color-backdrop | Behind dialogs |
Each tone has a solid color, a hover color, a text color (readable on the pale background) and a pale background.
| Tone | Solid | Hover | Text | Pale |
|---|---|---|---|---|
primary / .info | ||||
.success | ||||
.warning | ||||
.danger |
Variable names follow the pattern --color-{tone}, -hover, -text, -pale. Inside a component, a tone class exposes them as --tone, --tone-hover, --tone-text, --tone-pale, --tone-contrast — handy for your own components:
| Variable | Value |
|---|---|
--font-sans | Inter, then the system stack |
--font-mono | Menlo, Consolas, monospace |
--font-size · -sm · -xs · -2xs | 15px · 14px · 13px · 11px |
--line-height | 1.4667 (22px at 15px) |
--font-weight-medium · -semibold | 500 · 600 — Fernand never uses 700 in the UI |
| Variable | Value | |
|---|---|---|
--space-1 … --space-15 | 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 60px | |
--radius-sm · --radius · --radius-lg · --radius-pill | 3 · 4 · 5 · 999px | |
--control-height · -sm · -xs | 46 · 34 · 24px — inputs and buttons share them |
--shadow-ring--shadow-raised--shadow-card--shadow-popover--shadow-modalFernand's signature is a long, exponential ease-out: things move fast then settle softly. --ease: cubic-bezier(.19, 1, .22, 1) with --duration (.75s), --duration-fast (.5s) and --duration-instant (.2s, for dialogs and popovers). Motion is disabled when the user asks for prefers-reduced-motion.
From the Fernand manifest. The CSS gets you the look — these rules get you the feel.
Replying with R means there's also a Reply button. Show the shortcut in the button with a <kbd> so people learn it.
Esc, a click on the backdrop (closedby="any"), a close button, and a Cancel button.
Validate on the client, then show the result immediately — don't wait for the server. Adding a tag shows the tag right away.
Same title structure, same "add an entry" pattern, same way to validate changes — across every settings page.
Grey text, thin borders, one blue primary action per screen. Color is reserved for state: success, warning, danger.
Inter at 15px. Titles are light; section labels are small, uppercase and grey.
The h2 is the most recognisable Fernand element: it introduces every group of settings. Use <hgroup> for a title with a description:
Invite the people who answer your customers.
Nobody is waiting for an invitation.
Regular paragraph with a link, strong (500, never bold), emphasis, small text, highlighted, deleted, inserted, SLA, H2O and E=mc2.
A muted paragraph, for descriptions.
A subtle paragraph, for metadata.
Saved · Unsaved changes · Failed to send
Fernand is the calmest support tool we have ever used.
<code>, <pre> and <kbd>, as in the app's editor.
Insert {{ contact.first_name }} to personalise the reply.
curl https://api.getfernand.com/conversations \
-H "Authorization: Bearer $TOKEN"
Press ⌘ K to open the command bar, ? for the cheatsheet.
A plain <table> gets the uppercase header and thin separators.
| Teammate | Role | Replies | |
|---|---|---|---|
ALAntoine Lefeuvre |
Owner | 1,204 | |
CMCamille Martin |
Agent | 872 | |
JDJules Durand |
Pending | — |
Cells with .actions only appear on row hover (on devices that can hover). .numeric right-aligns with tabular figures.
| Domain | Status | Record |
|---|---|---|
| mail.acme.com | Verified | TXT |
| acme.io | Pending | CNAME |
| old-acme.com | Failed | MX |
Wrap a wide table in a <figure> to make it scroll horizontally on small screens. Use .compact for denser rows.
Every text-like <input>, <select> and <textarea> is styled. No class needed.
A <small> right after a field becomes its description.
Use aria-invalid — it styles the field and tells screen readers.
A <small> right after an invalid field turns red automatically:
The browser's :user-invalid is styled too: it only kicks in once the user has interacted with the field. Type something, then leave the field.
Add .shake to any element to signal a wrong value — the app does it on a wrong 2FA code.
Wrap the input in its <label> and you're done.
A checkbox with role="switch" — the toggle used everywhere in the settings.
A <label class="card"> with a radio inside becomes a selectable card. The selected ring comes from :has(input:checked) — no JavaScript.
role="group" glues fields, text and buttons together.
<button>, <input type="submit|button|reset"> and <a class="button"> all look the same.
type="reset" is a secondary action by nature, so it's outlined by default.
| Class | When to use it |
|---|---|
| none | The main action of a screen or a form. One per view. |
.secondary | An important action next to the primary one (e.g. "Save as draft"). |
.neutral | Toolbars and lists — the app's white, raised button. Turns blue on hover. |
.ghost | Cancel, back, dismiss. |
.link | Low-priority actions inside text ("Forgot password?"). |
aria-busy="true" shows a spinner and blocks clicks. aria-pressed turns a .neutral or .ghost button into a toggle.
A typical settings form, with zero layout classes except .grid.
.alert plus a tone. Add role="status" (or role="alert" for urgent messages) when it appears dynamically.
Default — a neutral piece of information.
Info — your help center is being rebuilt, it'll be live in a minute.
Success — your domain has been verified.
Warning — you've used 90% of your monthly replies.
Danger — we couldn't reach your mail server.
Add a payment method to keep your inbox running. See plans
.alert.confirm asks for a decision in place, with actions in a <footer>. Combine it with a tone.
Messages will be combined into the oldest conversation.
Conversations already tagged by it will keep their tags.
Publish your help center now?
Notice the buttons: they inherit the tone of the alert, so the "Delete rule" button is red without any extra class.
.alert.banner spans the full width and sticks to the top of its scroll container.
An <article> is a card. Its <header> and <footer> are laid out for you.
A thin ring instead of a border: cards feel light even when stacked.
Get notified in #support for every new conversation.
1,840 of 2,000 replies used.
A link or a button with .card lifts on hover. aria-current (or .selected) adds the blue focus ring, .new is the dashed "add" placeholder.
A soft drop shadow, for cards floating on a grey page.
Not available on your plan.
Tags are rounded pills with a colored dot. Badges are the small uppercase status labels.
Set any color with --dot-color, or use a tone on the tag.
Real ARIA tabs for in-page content, or a .tabs nav for links between pages.
Switching panels needs fernand.js (it also handles ← → Home End).
Paste this snippet before </body>.
For links between pages, use a <nav class="tabs"> and mark the current page with aria-current="page".
The native <dialog> element. Opening and closing need no JavaScript at all.
command="show-modal" + commandfor="id" on a button opens it.closedby="any" closes it on Esc and on a backdrop click.<form method="dialog"> closes it on submit — dialog.returnValue is the clicked button's value.Structure it with <header>, <section> (the scrollable body) and <footer>. Older browsers get the same behaviour from fernand.js.
dialog.confirm centers an icon and a question. The tone colors the icon and the buttons.
Sizes: .small (420px), default (550px), .medium (640px), .large (820px), .fullscreen, .sidepanel. Or set --dialog-width yourself.
The native popover attribute: light-dismiss, Esc and top-layer for free. Anchored under its button where CSS anchor positioning is supported.
Inside a popover, a <menu> (or <ul>) becomes a list of actions. .danger items turn red, <kbd> is pushed to the right, aria-checked="true" shows a check mark. Any other content gets a comfortable padding.
A data-tooltip attribute, pure CSS. Keep them short — and never put essential information only in a tooltip.
Short confirmations, top-right, gone in 5 seconds.
Fernand.toast(message, { tone, title, timeout }) creates the region on first use. timeout: 0 keeps it until dismissed.
Put toasts inside a <div class="toasts" aria-live="polite"> to stack them in the corner. Add .timed for the countdown bar (--toast-duration).
Initials or an image, round, with optional presence.
Use <img class="avatar" src="…" alt="…"> for pictures. Size with .small, .large or --avatar-size; color with a tone or --avatar-bg/--avatar-color.
Native <progress>, skeletons while data loads, and aria-busy.
The "wireframe" the app shows while a list loads. Prefer it to spinners for content.
Loading conversations…
Native <details>. Give them the same name to keep only one open.
Add a forwarding rule from support@yourdomain.com to the address shown in Settings → Channels.
Yes — from Help Scout, Zendesk, Front, Intercom and more.
Fernand works in any mobile browser and can be installed as an app.
Two layouts cover most of the app.
A 250px navigation on the left, a centered 800px column on the right. This very page uses it. On screens under 900px, the navigation moves on top.
Use <main class="wide"> for dashboards that need 1200px.
For login, sign-up and onboarding: a grey page with a 350px column (.wide for 640px). See the login example.
Few, on purpose. If you need many, write a component.
| Class | Effect |
|---|---|
.grid | Responsive columns, at least --grid-min (220px) wide, --grid-gap apart. |
.row | Horizontal flex, wraps, centered vertically, --row-gap (12px). Resets children margins. |
.stack | Vertical flex with --stack-gap (12px). |
.end | Pushes an element to the end of a .row or a footer. |
.center | Centers text. |
.muted · .subtle | Secondary and tertiary text colors. |
.caps | The small uppercase label style, on any element. |
.ellipsis · .tabular | Truncate with "…" · tabular figures. |
.sr-only | Visible to screen readers only. |
.only-light · .only-dark | Show an element in one theme only. |
Complete pages built only with Fernand CSS.