Typography skill.
A free typography skill that teaches your AI coding agent, from Claude Code to Cursor, to set type with a system: named text roles, clear size steps, line height, measure and fonts that load without a jump.
Type does most of the work in an interface, and it is where AI-made pages give themselves away first: five sizes that are almost the same, headings that run the width of the screen, body text too small to read. This skill gives your agent a type system to set up once, built on seven named text roles for marketing pages and app UI, and rules it applies on every page. When your project already has a type system, the agent keeps it and fixes how it is used.
- Skill
wensity-typography- Plan
- Free, no account needed
- Loads when
- Building a page or component, or changing fonts, sizes or text styling
- Works in
- Claude Code, Codex, Cursor, GitHub Copilot, Windsurf, Gemini CLI, OpenCode
- Format
- Agent Skills (SKILL.md), plain files in your project
- Pack
- One of the Wensity design skills for AI coding agents
Install
Run this in your project. It finds the agents you use and puts the skill where each one looks for it.
pnpm dlx skills add wensity/skills --skill wensity-typography
npx skills add wensity/skills --skill wensity-typography
yarn dlx skills add wensity/skills --skill wensity-typography
bunx skills add wensity/skills --skill wensity-typography
Open source under MIT, with the other free skills. A star helps other developers find them.
Or add it by hand: save the file as SKILL.md in a folder named wensity-typography.
Where each agent looks for it
- Claude Code
- .claude/skills/wensity-typography/SKILL.md
- Codex, Cursor, Copilot, Windsurf, Gemini CLI, OpenCode
- .agents/skills/wensity-typography/SKILL.md
When it loads
You never call a skill by name. Your agent reads the short description at the top of every installed skill and loads this one when the work calls for it. Requests like these bring it in:
- The typography on this page looks off and is hard to read. Fix it.
- Set up fonts and a type scale for this Next.js app.
- The headings feel generic. Make them look designed.
What it covers
- Choosing families
- One sans with a real weight range, and a second family only for a job the first cannot do, picked from what the product looks like in print.
- Seven text roles
- Display, title, heading, body, label, caption and code, with values for marketing pages and for dense app UI, wired into Tailwind CSS.
- Line height and tracking
- Line height drops as text grows, and large text is tracked tighter while body text is left alone.
- Measure and line breaks
- Paragraphs held to 60 to 75 characters, balanced headings, pretty-wrapped paragraphs and truncation only as a last resort.
- Numbers, code and links
- Tabular figures for anything that counts or lines up, code sized to match the text around it, and underlined links in running text.
- Font loading
- next/font in Next.js, or self-hosted WOFF2 with a matched fallback elsewhere, so the page does not jump when the font arrives.
Fix this article
One blog post, set twice with the same words, colours and spacing. Flip the switch above it to see eight sizes and seven weights give way to four named roles, line heights that drop as the text grows, and paragraphs held to 65ch.
8 sizes, 7 weights
We cut our first reply time from four hours to 38 minutes
Priya Raman, Head of Support · March 18, 2026 · 6 min read
In January our median first reply took 4 hours and 12 minutes. Nobody on the team was slow: tickets were sitting in the wrong queue. Here is what we changed, in the order we changed it.
The first fix was routing. Every ticket used to land in one shared inbox, and whoever was free took the next one from the top. Billing questions waited behind bug reports that needed an engineer, so a two-minute answer could sit for half a day. We split the inbox into three queues by the first field a customer fills in, and gave each queue one owner per shift.
“The fastest reply is the one that reaches the right person first. We got quicker by sorting, not by typing.”
Daniel Osei, support lead
The second fix was saved replies, written by the people who send them. We retired 140 old macros and kept 22, each one checked against a month of real tickets. Agents now edit a reply in seconds instead of writing one from scratch, and customers stopped getting answers to questions they never asked.
Median first reply: 4 h 12 min in January, 0 h 38 min in March · 1,284 tickets · weekdays, 9:00 to 18:00 CET
Without the skill: Eight sizes and seven weights fight for attention, the leading is cramped, and every line runs the full width of the column.
Key rules
- Roles, not sizes
- Components use role names. Only the role definition holds pixel values.
- Three weights at most
- Regular, medium and semibold. Size, color and space carry the rest of the hierarchy.
- A 16px floor for body text
- 14px in dense app UI, and 16px in mobile inputs so iOS Safari does not zoom in.
- No eyebrows, no all caps
- Small uppercase labels over headings are the clearest sign of a template.
- No serif, italic or gradient text
- Emphasis comes from size, weight and placement, never from paint or a costume face.
- Contrast on every text color
- 4.5:1 for body, secondary and placeholder text, and 3:1 for large text.
Catch a lone word
Both boxes open at a width where the default wrap leaves the heading a single word. Drag either edge: the left keeps stranding words, the right balances its headings and pretty-wraps the paragraph.
Without the skill
Everything the front desk needs to keep a clinic running
Reminders that cut missed appointments in half
Patients get a text two days before their visit and another on the morning itself. If they reply C, the slot opens to the waitlist.
With the skill
Everything the front desk needs to keep a clinic running
Reminders that cut missed appointments in half
Patients get a text two days before their visit and another on the morning itself. If they reply C, the slot opens to the waitlist.
The full skill
This is the whole skill, exactly as your agent reads it. Change anything you like once it is in your project: it is a plain file.
Type does most of the work in an interface. Get it right and a plain page looks designed; get it wrong and no amount of color or motion saves it. This skill gives you a system to set up once and rules to apply everywhere.
The values below are defaults. If the project already has a type system, keep it and improve how it is used. Replace it only when asked.
1. Choose families
- One family is usually enough. A good sans with a real weight range covers display, headings, body and UI. Add a second family only for a role the first cannot do: a display sans with character for a marketing page, or a monospace for code and data.
- Sans only: no serif and no italic. Headings, body, quotes and captions are all set upright in sans. A serif title or an italic line reads as a template costume on a web product, so leave both out even on blogs, docs and luxury pages; carry tone with weight, size and spacing.
- Never more than two families plus a monospace. A third text family reads as indecision.
- Pick for the subject, not by habit. Do not put the same face on every project. Ask what the product's world looks like in print or on hardware, then choose a face with that character:
| The product feels like | Reach for | Free examples |
|---|---|---|
| a precise tool (dev tools, data, infra) | a tight grotesk, mono for data | Geist, Switzer, Onest; Geist Mono, JetBrains Mono |
| calm money or health (finance, insurance, clinics) | a steady neo-grotesk or humanist sans | Inter, Public Sans, Source Sans 3 |
| friendly consumer (social, food, travel, education) | a rounder geometric or humanist sans | Plus Jakarta Sans, Figtree, Nunito Sans, DM Sans |
| a confident brand or startup marketing page | a sans with personality at display sizes | General Sans, Manrope, Bricolage Grotesque, Space Grotesk |
| reading and publishing (docs, blogs, magazines) | a calm sans with a true text cut, tight titles in the same family | Inter, Satoshi, Source Sans 3 |
| fashion, editorial or luxury | a refined display sans set large and tight, titles only | Satoshi, General Sans, Clash Grotesk |
- Check the face can do the job: the weights you need (usually 400, 500, 600), tabular figures, good punctuation, the languages you ship, and a variable version if you use several weights.
2. Define roles, not sizes
Name text by its job. Components use role names; only the role definition holds pixel values. Seven roles cover almost every interface:
| Role | Used for | Marketing page | App UI |
|---|---|---|---|
display | hero headline, one per page | 48 to 80px, weight 600, line height 0.95 to 1.05, tracking -0.03em to -0.04em | rarely used |
title | page and section headings | 32 to 44px, weight 600, line height 1.1, tracking -0.02em | 20 to 24px, weight 600, line height 1.25, tracking -0.01em |
heading | card, panel and subsection headings | 20 to 24px, weight 600, line height 1.25, tracking -0.01em | 15 to 16px, weight 600, line height 1.4 |
body | paragraphs | 16 to 18px, weight 400, line height 1.55 to 1.65 | 14 to 15px, weight 400, line height 1.5 |
label | buttons, inputs, nav, table headers | 14 to 15px, weight 500, line height 1.3 | 13 to 14px, weight 500, line height 1.3 |
caption | helper text, metadata, timestamps | 13 to 14px, weight 400, line height 1.45 | 12 to 13px, weight 400, line height 1.4 |
code | inline code, code blocks, IDs | 0.9em of the surrounding text, mono | same |
Rules for the scale:
- Steps must be obvious. Neighboring roles differ by at least about 20% in size, or by a size step plus a weight step (14px 500 label over 13px 400 caption is fine; 15px 400 next to 16px 400 doing different jobs is not).
- A ratio helps, but the table wins. Marketing pages suit a ratio of 1.25 to 1.333 between steps; dense app UI suits 1.125 to 1.2. Round to whole pixels.
- Three weights maximum: regular (400), medium (500) for labels, semibold (600) for headings and display. Hierarchy also comes from size, color and space, so it does not need five weights. If 600 looks too heavy for a large display line, use 500 for display rather than adding a fourth weight.
- Fluid size only for display and title on marketing pages:
font-size: clamp(2.5rem, 1.5rem + 4vw, 5rem). Keep body and UI text fixed so dense layouts stay predictable. - Body text floor: 16px on marketing and reading pages, 14px in dense app UI. Input text is 16px on mobile, or iOS Safari zooms the page when the field is focused.
In Tailwind CSS v4, define the roles once (replace the family names with the ones you chose):
@theme {--text-display: clamp(2.75rem, 1.6rem + 4.2vw, 5rem);--text-display--line-height: 1;--text-display--letter-spacing: -0.035em;--text-display--font-weight: 600;--text-title: 2.25rem;--text-title--line-height: 1.1;--text-title--letter-spacing: -0.02em;--text-title--font-weight: 600;--text-heading: 1.25rem;--text-heading--line-height: 1.3;--text-heading--letter-spacing: -0.01em;--text-heading--font-weight: 600;--text-body: 1rem;--text-body--line-height: 1.6;--text-label: 0.875rem;--text-label--line-height: 1.3;--text-label--font-weight: 500;--text-caption: 0.8125rem;--text-caption--line-height: 1.45;}
Then use text-display, text-title, text-body in markup instead of
text-[44px] or a chain of text-4xl leading-tight tracking-tight. In
Tailwind v3 put the same values under theme.extend.fontSize as
[size, { lineHeight, letterSpacing, fontWeight }] tuples.
3. Line height and letter spacing
- Line height goes down as size goes up. Display 0.95 to 1.05, titles 1.1 to 1.2, body 1.5 to 1.65, labels 1.2 to 1.35. A 64px headline at line height 1.5 falls apart into separate lines.
- Tight tracking for big text, none for body. Display -0.03em to -0.04em, titles -0.02em, headings -0.01em, body and labels 0. Do not go tighter than -0.05em; letters start to touch. Tune by eye for the face you chose.
- No positive tracking on lowercase body and interface text. Positive tracking only helps all-caps text, and all-caps text should be rare (see section 6).
- Wide lines need more leading. If body text runs near 75 characters, use line height 1.6 or more; at 50 characters, 1.5 is enough.
- Light text on dark backgrounds looks heavier and tighter. On dark themes add about 0.05 to body line height.
4. Measure and line breaks
- Body text: 60 to 75 characters per line. Set
max-width: 65chon paragraphs, not on the whole page. Captions and helper text can run narrower. - Display headlines: about 12 to 20 characters per line. A headline
running across a 1440px screen in one line has no shape. Constrain it with
max-widthinchor a container width. - Balance headings, pretty-wrap paragraphs:
h1, h2, h3, .text-display, .text-title { text-wrap: balance; }p, li, figcaption { text-wrap: pretty; }
Both are progressive: older browsers ignore them. balance evens out
headline lines; pretty stops a single word sitting alone on the last
line of a paragraph.
- Hand breaks only between phrases, and check them at every width the
text renders at. A
<br>that lands mid-line on a phone is worse than the default wrap. Prefermax-widthover<br>. - Long words and URLs in user content need
overflow-wrap: anywhereon their container, or they push the layout sideways on mobile. - Truncation is a last resort. Truncate single-line labels in tight UI
(
truncatein Tailwind). If the hidden part matters, make the full text reachable by keyboard and touch (a tooltip on a focusable element, an expandable row, a detail view); atitleattribute alone is not enough. Never truncate body text or anything the user must read to act. - Multi-line clamps (
line-clamp-2): keep line height at 1.3 or more on the clamped text so descenders (g, y, p) are not cut off. Do not add bottom padding inside the clamp; it shows the top of the next line.
5. Text color and contrast
- Three text colors are enough: primary (headings and body), secondary (supporting text, captions, placeholders), and disabled. Define them as tokens. Avoid making a fourth by lowering opacity on the fly.
- Contrast floors (WCAG 2.2 AA): 4.5:1 for body and small text, 3:1 for text 24px and up (or 18.66px bold). Secondary text and placeholder text also need 4.5:1. Only text in disabled controls is exempt.
- On dark backgrounds use off-white, not pure white, for long text (around 90 to 94% lightness). Pure white on near-black glares.
- On colored or tinted surfaces, tint secondary text toward the surface hue instead of using a neutral grey, which looks dirty on color.
6. What to avoid
- Eyebrows and kickers: small uppercase letter-spaced labels above a heading ("FEATURES", "WHY US", "HOW IT WORKS"). Do not use them. They are the clearest sign of template UI. If a heading needs context, write a better heading.
- All-caps labels in general, including small section headings in sidebars, settings panels, cards, footers and demo controls. Sentence case reads faster; give a small heading weight (500 or 600) and the secondary text color instead. Reserve caps for genuine abbreviations (API, URL).
- Gradient text and one colored word in a headline. Emphasis comes from size, weight and placement, not paint.
- Italic, anywhere. Use weight for emphasis, and set quotes upright with a rule or a larger size beside them.
- Serif faces. Display serifs and text serifs both read as a costume on a web product. Pick a sans with character instead.
- Monospace as decoration. Mono is for code, data, IDs, keyboard keys and numbers in columns. Mono labels on a marketing page are a costume.
- Centered paragraphs longer than two lines. Center short headlines and single-line CTAs only.
- Justified text on the web. Without hyphenation it makes rivers of white space.
- Underlines for anything but links.
7. Numbers, code and data
- Tabular figures in anything that aligns or updates: tables, prices,
timers, counters, stats.
font-variant-numeric: tabular-nums;(Tailwindtabular-nums). Without it, a counting number jitters sideways as digit widths change. - Slashed zero for IDs and codes where 0 and O can be confused:
font-variant-numeric: tabular-nums slashed-zero;. - Align numbers at the end of table cells (
text-align: end) so decimal places line up. Text columns align at the start. - Code: monospace at about 0.9em of the surrounding text so its x-height matches. Inline code gets a subtle background and 0.2em horizontal padding, no border. Code blocks get line height about 1.6 and horizontal scroll, never wrapping that breaks indentation.
- Keyboard keys (
<kbd>) in the mono face at caption size.
8. Links
Underline links inside running text; color alone fails for color-blind
readers. Scope it to prose so navigation lists and buttons are not
underlined (.prose is whatever wraps your article or long-form content):
:where(p, td, dd, figcaption, blockquote) a,:where(.prose, article) li a {text-decoration-line: underline;text-decoration-thickness: 1px;text-underline-offset: 0.2em;}
Navigation, buttons and card links do not need underlines; their position already says they are interactive.
9. Load fonts without layout shift
- Next.js: use
next/font(next/font/googleornext/font/local). It self-hosts, preloads and generates a fallback with matching metrics, so the page does not jump when the font arrives. Expose it as a CSS variable and map the theme font to it with@theme inline(so the variable is read where it is used, not resolved once at:root):
// app/layout.tsximport { Onest } from "next/font/google";const sans = Onest({ subsets: ["latin"], variable: "--font-brand", display: "swap" });// <html lang="en" className={sans.variable}>
@theme inline {--font-sans: var(--font-brand), ui-sans-serif, system-ui, sans-serif;}
- Optical size: a face with an
opszaxis (Inter, Roboto Flex, Fraunces) only gets it if you load it:Inter({ subsets: ["latin"], axes: ["opsz"] }). Keepfont-optical-sizing: autoso small text gets the sturdier cut and large text the finer one. - Elsewhere: self-host WOFF2,
font-display: swap, preload only the one or two files used above the fold, and setsize-adjust,ascent-overrideanddescent-overrideon a local fallback face so the swap does not shift layout. - Load only what you use. Prefer one variable font file over six static weights. Subset to the scripts you ship.
10. Rendering details
-webkit-font-smoothing: antialiasedon dark themes makes light text look less bloated on macOS. Leave light themes at the default.- Set
::selectionto the brand tint and keep it readable. The browser's default blue is a sign the page was not finished. - Use real punctuation in copy: curly quotes (“ ” ‘ ’), the ellipsis character (…), and the multiplication sign (×) in dimensions.
- Respect user settings: size text in
rem, never block zoom, and check that text still fits at 200% text size and with larger letter and line spacing.
11. Check before you finish
- Every text style comes from a named role; no stray pixel sizes.
- Neighboring roles are clearly different in size, or size plus weight.
- At most two families plus mono, at most three weights, all loaded.
- Body text 16px (14px dense UI), inputs 16px on mobile.
- Paragraphs capped at about 65ch; display lines about 12 to 20 characters.
- Headings balanced, paragraphs pretty-wrapped.
- No eyebrows, no gradient text, no colored single word, no serif, no italic.
- Tabular figures on every changing or aligned number.
- Text and placeholder contrast passes 4.5:1 (3:1 for large text) in every theme.
- Fonts load with no visible layout shift.
- Checked at 390px and 1440px with the real copy, longest strings included, and at 200% text size.
Works well with
Layout and spacing
FreeA 4px spacing scale, grouping by distance, alignment, containers, grids and responsive structure.
UX writing
FreeHeadings, buttons, error messages, empty states and realistic sample content, all written without filler.
Color and dark mode
ProOKLCH role tokens, true neutral greys, one accent, both themes, and a script that checks contrast and gamut.