Design guide

Build interfaces that feel like Fernand.

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.

Installation

One stylesheet, one optional script. No build step.

Copy the css/, fonts/ and (optionally) js/ folders, then link them:

Starter template

What's inside

FileRole
css/fernand.cssThe framework. Tokens, base, forms, components, utilities — in a single @layer fernand.
js/fernand.jsOptional, ~8 KB unminified. ARIA tabs, theme switcher, Fernand.toast(), fallbacks for command and closedby.
fonts/Inter, the same files as the app.

Principles

Four ideas you need to know before writing any markup.

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.

2. Classes change behaviour

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.

3. Tones everywhere

.info, .success, .warning and .danger recolor any component: buttons, alerts, badges, tags, toasts, progress bars, table rows…

4. Variables, not overrides

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.

The same button, three ways

Tones on different components

Churned Urgent

Dark & light mode

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 writeResult
nothingFollows 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

A dark island in a light page

Always light

Active

data-theme="light"

Always dark

Active

data-theme="dark"

Theme switcher

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.

Images per theme

Fernand Fernand Shown with .only-light / .only-dark

Your own colors

Use light-dark() for any custom color and it will follow the theme automatically:

Variables

Change a token once, every component follows. Swatches below are live: switch the theme to see dark values.

Overriding

Palette — same names as the app

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.

VariableVariable
--white--black
--grey-light--grey-medium
--grey-strong--border / --border-strong
--blue--blue-pale
--green--green-pale
--orange--orange-pale
--red--red-pale

Semantic colors

VariableUsed for
--color-bgPage background, dialogs
--color-bg-subtleSide areas, card & dialog footers, table heads
--color-surfaceCards, tags, accordions
--color-surface-raisedPopovers, menus, tooltips
--color-field · --color-field-border · --color-field-disabledInputs, selects, textareas
--color-textBody text, titles
--color-text-mutedLabels, descriptions, secondary text
--color-text-subtleSection labels (h2), placeholders, icons
--color-border · --color-border-strongSeparators, outlines
--color-kbdKeyboard hints
--color-backdropBehind dialogs

Tones

Each tone has a solid color, a hover color, a text color (readable on the pale background) and a pale background.

ToneSolidHoverTextPale
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:

Typography

VariableValue
--font-sansInter, then the system stack
--font-monoMenlo, Consolas, monospace
--font-size · -sm · -xs · -2xs15px · 14px · 13px · 11px
--line-height1.4667 (22px at 15px)
--font-weight-medium · -semibold500 · 600 — Fernand never uses 700 in the UI

Spacing, shapes & controls

VariableValue
--space-1 … --space-154 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 60px
--radius-sm · --radius · --radius-lg · --radius-pill3 · 4 · 5 · 999px
--control-height · -sm · -xs46 · 34 · 24px — inputs and buttons share them

Elevation

--shadow-ring
--shadow-raised
--shadow-card
--shadow-popover
--shadow-modal

Motion

Fernand'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.

UX rules

From the Fernand manifest. The CSS gets you the look — these rules get you the feel.

  1. Every keyboard action has a visible equivalent.

    Replying with R means there's also a Reply button. Show the shortcut in the button with a <kbd> so people learn it.

  2. A dialog can always be dismissed four ways.

    Esc, a click on the backdrop (closedby="any"), a close button, and a Cancel button.

  3. Happy assumptions.

    Validate on the client, then show the result immediately — don't wait for the server. Adding a tag shows the tag right away.

  4. Settings sections look the same.

    Same title structure, same "add an entry" pattern, same way to validate changes — across every settings page.

  5. Quiet by default.

    Grey text, thin borders, one blue primary action per screen. Color is reserved for state: success, warning, danger.

Typography

Inter at 15px. Titles are light; section labels are small, uppercase and grey.

Heading 1 — page title

Heading 2 — section label

Heading 3 — card title

Heading 4 — sub title

Heading 5
Heading 6

The h2 is the most recognisable Fernand element: it introduces every group of settings. Use <hgroup> for a title with a description:

Teammates

Invite the people who answer your customers.

Pending invitations

Nobody is waiting for an invitation.

Inline text

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

Lists & quotes

  • Unordered item
  • Another item
  • Third item
  1. First step
  2. Second step
  3. Third step
Plan
Growth, billed yearly
Seats
8 of 10 used
Fernand is the calmest support tool we have ever used.
— A happy customer

Code & shortcuts

<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.

Tables

A plain <table> gets the uppercase header and thin separators.

TeammateRoleReplies
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.

Boxed & tinted

DomainStatusRecord
mail.acme.comVerifiedTXT
acme.ioPendingCNAME
old-acme.comFailedMX

Wrap a wide table in a <figure> to make it scroll horizontally on small screens. Use .compact for denser rows.

Inputs

Every text-like <input>, <select> and <textarea> is styled. No class needed.

Two ways to label

Help text

A <small> right after a field becomes its description.

Your help center will live at acme.fernand.help

Types

Select & textarea

States & sizes

Other inputs

Validation

Use aria-invalid — it styles the field and tells screen readers.

This email address is not valid.

A <small> right after an invalid field turns red automatically:

Domains can't contain two consecutive dots.

Native validation

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.

Shake

Add .shake to any element to signal a wrong value — the app does it on a wrong 2FA code.

Checkboxes & switches

Wrap the input in its <label> and you're done.

Notify me about
Reply as

Switch

A checkbox with role="switch" — the toggle used everywhere in the settings.

Choice cards

A <label class="card"> with a radio inside becomes a selectable card. The selected ring comes from :has(input:checked) — no JavaScript.

Input groups

role="group" glues fields, text and buttons together.

https:// .fernand.help
Keep it secret. Regenerate it if it leaks.

Buttons

<button>, <input type="submit|button|reset"> and <a class="button"> all look the same.

Default elements

a.button

type="reset" is a secondary action by nature, so it's outlined by default.

Variants

ClassWhen to use it
noneThe main action of a screen or a form. One per view.
.secondaryAn important action next to the primary one (e.g. "Save as draft").
.neutralToolbars and lists — the app's white, raised button. Turns blue on hover.
.ghostCancel, back, dismiss.
.linkLow-priority actions inside text ("Forgot password?").

Tones

Sizes

Icons & shortcuts

States

aria-busy="true" shows a spinner and blocks clicks. aria-pressed turns a .neutral or .ghost button into a toggle.

Button groups

Full form

A typical settings form, with zero layout classes except .grid.

Profile

Added at the end of every reply you send.

Notifications


Alerts

.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.

With a title, a link and a close button

Your trial ends in 3 days

Add a payment method to keep your inbox running. See plans

Confirm

.alert.confirm asks for a decision in place, with actions in a <footer>. Combine it with a tone.

Merge these 2 conversations?

Messages will be combined into the oldest conversation.

Delete this rule?

Conversations already tagged by it will keep their tags.

Everything looks good

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.

Banner

.alert.banner spans the full width and sticks to the top of its scroll container.

Page content…

Cards

An <article> is a card. Its <header> and <footer> are laid out for you.

Simple card

A thin ring instead of a border: cards feel light even when stacked.

Slack

Connected

Get notified in #support for every new conversation.

Monthly replies

1,840 of 2,000 replies used.

Resets on November 1st.

Clickable cards

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.

Raised, disabled & empty

Raised

A soft drop shadow, for cards floating on a grey page.

Disabled Soon

Not available on your plan.

Tags

No tags yet Tags help you sort conversations by topic.

Tags & badges

Tags are rounded pills with a colored dot. Badges are the small uppercase status labels.

Tags

No color Billing Feature request Onboarding Bug Small + 3 more

Set any color with --dot-color, or use a tone on the tag.

Interactive

Link tag Removable JCJane Cooper

Badges

Trialing New Active Past due Churned

Dots & counters

Online Away Offline Unknown 4 12 99+

Tabs

Real ARIA tabs for in-page content, or a .tabs nav for links between pages.

ARIA tabs

Switching panels needs fernand.js (it also handles ← → Home End).

Segmented

Underline

Navigation tabs

For links between pages, use a <nav class="tabs"> and mark the current page with aria-current="page".

Vertical

Dialogs

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.

Form dialog

Rename inbox

Confirmation

dialog.confirm centers an icon and a question. The tone colors the icon and the buttons.

Delete this inbox?

1,204 conversations will be moved to the trash. This can't be undone.

Publish your help center?

It will be visible to anyone at acme.fernand.help.

Sizes & side panel

Keyboard shortcuts

Every action has a shortcut, and every shortcut has a button.

ReplyR
Add a noteN
Assign to meI
Close conversationE
Command bar⌘ K
JC

Jane Cooper

jane@acme.com

Subscription

Plan
Growth Active
Customer since
March 2024
Lifetime value
$1,280

Tags

VIP Billing

Sizes: .small (420px), default (550px), .medium (640px), .large (820px), .fullscreen, .sidepanel. Or set --dialog-width yourself.

From JavaScript

Popovers & menus

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.

Tooltips

A data-tooltip attribute, pure CSS. Keep them short — and never put essential information only in a tooltip.

Native title

Toasts

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.

Markup

Your changes have been saved.

Put toasts inside a <div class="toasts" aria-live="polite"> to stack them in the corner. Add .timed for the countdown bar (--toast-duration).

Avatars

Initials or an image, round, with optional presence.

AL AL AL CM JD PB SK ON AW
+4 JD CM AL
Viewing this conversation

Use <img class="avatar" src="…" alt="…"> for pictures. Size with .small, .large or --avatar-size; color with a tone or --avatar-bg/--avatar-color.

Progress & loading

Native <progress>, skeletons while data loads, and aria-busy.

Skeleton

The "wireframe" the app shows while a list loads. Prefer it to spinners for content.

Busy

Loading conversations…

Accordion

Native <details>. Give them the same name to keep only one open.

How do I forward my support email?

Add a forwarding rule from support@yourdomain.com to the address shown in Settings → Channels.

Can I import my old conversations?

Yes — from Help Scout, Zendesk, Front, Intercom and more.

Is there a mobile app?

Fernand works in any mobile browser and can be installed as an app.

Page layouts

Two layouts cover most of the app.

Settings layout

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.

Centered layout

For login, sign-up and onboarding: a grey page with a 350px column (.wide for 640px). See the login example.

Utilities

Few, on purpose. If you need many, write a component.

ClassEffect
.gridResponsive columns, at least --grid-min (220px) wide, --grid-gap apart.
.rowHorizontal flex, wraps, centered vertically, --row-gap (12px). Resets children margins.
.stackVertical flex with --stack-gap (12px).
.endPushes an element to the end of a .row or a footer.
.centerCenters text.
.muted · .subtleSecondary and tertiary text colors.
.capsThe small uppercase label style, on any element.
.ellipsis · .tabularTruncate with "…" · tabular figures.
.sr-onlyVisible to screen readers only.
.only-light · .only-darkShow an element in one theme only.
1
2
3
4
Assigned Antoine

Examples

Complete pages built only with Fernand CSS.