NS Design System
v3.0.0 · 257 tokens

05Typography

Two self-hosted faces. Switzer is the whole interface AND the reading layer — one grotesque separated by weight and size rather than a display/text pair, and it sets the quotations too; Roboto Mono is the data voice, and it is shipped rather than borrowed from the OS because it sets every index, duration, tag and status on every screen. Switzer is an Indian Type Foundry cut from Fontshare, Roboto Mono is under the SIL OFL — 67 KB for the pair, licences in fonts/. Reading copy is 14 px: this is a product with an app inside it, and the compact scale is after openclaw/carapace. This page is the whole typographic contract: the scale, the reading weight, the effects, the voice, and the accessibility floor. Open the full specimen ↗

The pairing

// Switzer 700 · heading Build on the platform, not around it The same face carries the reading layer — lesson copy, descriptions, and every sentence longer than a label. Nothing changes but weight and size, which is why the page reads as one voice rather than two negotiating. 01 · APEX BASICS · 12:40 · COMPLETE

The fourth voice — and the one font this system does not ship

--font-serif · font-serif · no binary shipped
The hairline is the structure. Everything else is negotiable.

Display speaks, Text explains, Mono keeps the records — and Serif quotes. A pull-quote, a drop cap and a section quotation are the places a page stops arguing and starts citing; set in the same grotesque as the argument, they never quite read as a different speaker, they read as a bigger heading. Compare the two above.

No font file is shipped for it. The N&M family is self-hosted because it is the brand; a face used on three devices per page does not earn another download, and this system ships no licence for one. --font-serif resolves to the reader’s own Georgia — a genuinely good screen serif present on essentially every device — and a product that has licensed a display serif overrides exactly one token to get it. The pattern is borrowed from openclaw/carapace, which ships font stacks and no font files at all.

The three voices

Switzer · wght 100–900 · shipped, 29 KB
700 — hero and h1–h3
600 — h4, card titles, buttons, inline emphasis
500 — nav links, tab labels
400 — body copy, the reading default
quotations · the platform serif · NOT shipped
The hairline is the structure. Everything else is negotiable.
Roboto Mono · wght 100–700 · shipped, 37 KB · the data voice
400 — code blocks, inline code, timestamps
700 — kickers · indexes · status labels

Mono is the material Principle 2 is made of — every index, duration, timestamp and status runs through it — and it is still the one face this system does not ship. Those runs are short, tracked and uppercase, and the reader’s own console face is not merely adequate there, it is more familiar than anything we could send them. The consequence to know: mono is SF Mono on macOS, Consolas on Windows, and whatever the distro sets on Linux, so a mono run is not an identical width across platforms. The tabular-nums in tokens/base.css is what keeps digits aligned.

Why body copy is 400

The previous family was a Nunito-derived cut whose true Regular rendered grey rather than black, so this system set reading copy at 450 (“Book”) — an interpolation step invented to fix that face’s problem. Switzer does not have it: its Regular is properly fitted, and carrying 450 across would have been cargo — half a step heavier than the designer drew, for no reason anyone could still state. Body is 400. Switzer also has a real 500, which the old family lacked, so the ramp is 400 / 500 / 600 / 700 and every step is a weight that was actually drawn.

The scale

--size-displayclamp(2rem, 1.55rem + 2.2vw, 3.25rem) Namaste Salesforce
--size-h12.5rem Namaste Salesforce
--size-h22rem Namaste Salesforce
--size-h31.5rem Namaste Salesforce
--size-h41.25rem Namaste Salesforce
--size-body-lg1.0625rem Namaste Salesforce
--size-body0.875rem Namaste Salesforce
--size-small0.8125rem Namaste Salesforce
--size-fine0.75rem Namaste Salesforce
--size-label0.6875rem Namaste Salesforce
--size-mono0.8125rem Namaste Salesforce

--size-mega is left out of this table on purpose — at clamp(2.75rem, 1.9rem + 4.2vw, 5.5rem) its preview row would be taller than everything above it put together. It is shown in place under Display & poster typography below, which is the only context it belongs in.

Compact, after carapace: 12 / 13 / 14 / 17 / 20 / 24 / 32, then the ratio opens back up above 32 because a hero is a different job from a heading. Reading copy is 14 px because this is a product with an app inside it — at 16 the player, admin and tables all had to fight the base size with --size-small, which is the tell that the base was wrong for most of the screens being built. Only the top two steps clamp: a hero has to survive a 360 px phone, a paragraph does not. Everything is rem, because one px font-size is one piece of text that stops responding to browser zoom.

Weight, leading, tracking

--tracking-labelvar(--ns-tracking-label)
--tracking-tightvar(--ns-tracking-tight)
--tracking-megavar(--ns-tracking-mega)
--tracking-widevar(--ns-tracking-wide)
--weight-label700
--weight-heading700
--weight-semibold600
--weight-medium500
--weight-body400
--weight-regular400
--weight-body-strong600
--leading-tightvar(--ns-leading-tight)
--leading-headingvar(--ns-leading-heading)
--leading-bodyvar(--ns-leading-body)
--leading-megavar(--ns-leading-mega)

Measure — the most common typographic mistake in a product

--measure-prose · 68ch · correct

The reading measure is a character count, not a width — the constraint IS characters per line, which makes ch the one honest unit for it. Past roughly eighty characters the eye starts losing the line return, and reading speed measurably drops.

no cap · what a full-width paragraph does

The reading measure is a character count, not a width — the constraint IS characters per line, which makes ch the one honest unit for it. Past roughly eighty characters the eye starts losing the line return, and reading speed measurably drops. This paragraph is set with no cap at all, and the difference is not subtle at this width.

--measure-narrow · 46ch · ledes, quotes, callouts

Short-measure text can afford a larger size, because the line is short enough to return from.

Numerals — data is never proportional

tabular · what a column of data uses
11:04
18:41
09:15
proportional · the same three values, not lining up
11:04
18:41
09:15

Alignment in a column of figures is a typographic property, not a table one — so time, output, .ns-num and anything carrying [data-numeric] get tabular-nums in the base layer, everywhere, automatically.

Heading Scale

Switzer 500–700 — display down to h4

open ↗

Body Copy

Switzer 400 at 14px — the reading weight and size

open ↗

Kicker / Code-Comment Label

Section eyebrows render as an Apex-style code comment, not a pastel pill

open ↗

Code / Mono

System mono in the Salesforce-navy console chrome

open ↗

Text effects — the three rules

Everything below is decoration over text that already reads correctly without it. 1. The effect is never the meaning — struck text is a real <s>, a highlight is a real <mark>, a citation is a real link to a footnote that exists. 2. It draws once — these fire when scrolled into view and then stop; a thing that loops forever mid-paragraph is an ad. 3. It collapses under reduced motion — each effect's final state is the correct rendering, so flattening leaves a drawn highlight rather than an invisible one. Source: components/css/type-fx.css + assets/js/type-fx.js.

Highlight marks a phrase, never a paragraph — and four tints exist so one page can mark two different things: shipped, deprecated, removed, or filled solid.

One record at a time collections, always — the correction pattern: struck text goes muted so the eye lands on the live value.

Bulkify every trigger — one circled phrase per screen.

Governor limits   RunLocalTests   optional

Developer Console

Metadata deploy complete

markup
<mark class="ns-mark ns-mark--animate">a highlighter marks a phrase</mark>
<s class="ns-strike ns-strike--animate ns-strike--muted">one record at a time</s>
<span class="ns-circle ns-circle--animate">every trigger<svg viewBox="0 0 220 60" preserveAspectRatio="none" aria-hidden="true"><path d="M28,10 C90,-2 200,2 210,22 …"/></svg></span>
<span class="ns-frame ns-frame--animate">Governor limits</span>
<h2 class="ns-scan ns-scan--lines">Developer Console</h2>
<h2 class="ns-scramble">Metadata deploy complete</h2>

Responsive shift — why the scramble exists

The scrambler's real job is not the entrance; it is the break. When a breakpoint re-wraps a heading, the text normally teleports to its new position. assets/js/type-fx.js listens to the breakpoint media queries — not to resize, which would fire on every frame — and re-settles the characters, so a reflow reads as deliberate rather than as a jump. For the layout half of the same problem, .ns-shift-group hands the browser interpolate-size and transitions on font-size, line-height, letter-spacing and max-inline-size, and .ns-shift takes a view-transition-name. Both degrade to nothing where unsupported, which is the right failure mode for a nicety. Resize this page across 40rem or 64rem to watch the heading above re-settle.

Text Effects

Highlight, strike, circle, frame, scan and scramble — each drawn once, over text that already reads without it

open ↗

Display and poster typography

Type at --size-mega is a graphic element, not a heading: it sets solid (--leading-mega, 0.94), tightens to --tracking-mega because tracking scales with the size it is set at, and is laid out against the viewport rather than a container.

Build onthe platform

One signal colour, two steps of it

The hairline is the structure. Everything else is negotiable.

Display Typography

Poster scale, outline, gradient, stacked, drop cap, pull-quote and the kinetic strip

open ↗

Circular text, links, citations, anchors

Circular Text, Links & Citations

Type on a curve, the three link treatments, footnote citations and heading anchors

open ↗

Content design — how the product speaks

Voice is a typographic decision, which is why it lives here rather than on a page of its own: plain English, encouraging, practical; explain why, not just what; second person for instruction, third for the product. Sentence case everywhere except uppercase mono kickers and lesson-type badges — and uppercase is a two-or-three-word device, because a tracked uppercase sentence is measurably slower to read. No emoji in product copy. Numbers only when they are concrete.

Do

  • Buttons name the action — “Start learning”, “Send sign-in link”, “Browse courses”.
  • Errors say what happened and what to do — “We could not find an account for that address. Create one instead.”
  • Empty states have three parts — what is missing, why it is empty, and one way out.
  • Labels are nouns, actions are verbs. A control that reads as a noun is not a control.

Don't

  • “Submit”, “Click here”, “OK” — a mechanism, not an action.
  • “Error: authentication failed (code 401)” — a status code is never the whole message.
  • Blaming the reader: “you entered an invalid…”.
  • Title Case On Every Heading. It reads as a brochure, and it breaks the sentence-case rule the whole system runs on.

Content Design

Voice, casing, and the exact words for errors, empty states and buttons

open ↗

Accessibility — the typographic floor

Weight, measure and leading are accessibility properties before they are aesthetic ones. Reading copy is --weight-body (450) and never lighter; body leading is 1.65, above the 1.5 that WCAG 1.4.12 needs to survive a reader's own text-spacing overrides; every size is rem so browser zoom works; secondary ink clears 6.87:1 rather than sitting at the 4.5:1 line. The general component contract — focus, targets, motion, status colour — is on the Accessibility page.

Typographic Accessibility

Reading weight, measure, contrast, zoom, and what an effect may never be the only signal for

open ↗