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

Install

Run this in your project. It finds the agents you use and puts the skill where each one looks for it.

terminal
pnpm dlx skills add wensity/skills --skill wensity-typography
wensity/skills

Open source under MIT, with the other free skills. A star helps other developers find them.

Star on GitHub

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 likeReach forFree examples
a precise tool (dev tools, data, infra)a tight grotesk, mono for dataGeist, Switzer, Onest; Geist Mono, JetBrains Mono
calm money or health (finance, insurance, clinics)a steady neo-grotesk or humanist sansInter, Public Sans, Source Sans 3
friendly consumer (social, food, travel, education)a rounder geometric or humanist sansPlus Jakarta Sans, Figtree, Nunito Sans, DM Sans
a confident brand or startup marketing pagea sans with personality at display sizesGeneral 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 familyInter, Satoshi, Source Sans 3
fashion, editorial or luxurya refined display sans set large and tight, titles onlySatoshi, 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:

RoleUsed forMarketing pageApp UI
displayhero headline, one per page48 to 80px, weight 600, line height 0.95 to 1.05, tracking -0.03em to -0.04emrarely used
titlepage and section headings32 to 44px, weight 600, line height 1.1, tracking -0.02em20 to 24px, weight 600, line height 1.25, tracking -0.01em
headingcard, panel and subsection headings20 to 24px, weight 600, line height 1.25, tracking -0.01em15 to 16px, weight 600, line height 1.4
bodyparagraphs16 to 18px, weight 400, line height 1.55 to 1.6514 to 15px, weight 400, line height 1.5
labelbuttons, inputs, nav, table headers14 to 15px, weight 500, line height 1.313 to 14px, weight 500, line height 1.3
captionhelper text, metadata, timestamps13 to 14px, weight 400, line height 1.4512 to 13px, weight 400, line height 1.4
codeinline code, code blocks, IDs0.9em of the surrounding text, monosame

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

Source
@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: 65ch on 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-width in ch or a container width.
  • Balance headings, pretty-wrap paragraphs:
Source
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. Prefer max-width over <br>.
  • Long words and URLs in user content need overflow-wrap: anywhere on their container, or they push the layout sideways on mobile.
  • Truncation is a last resort. Truncate single-line labels in tight UI (truncate in 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); a title attribute 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; (Tailwind tabular-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.

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

Source
: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/google or next/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):
Source
// app/layout.tsx
import { Onest } from "next/font/google";
const sans = Onest({ subsets: ["latin"], variable: "--font-brand", display: "swap" });
// <html lang="en" className={sans.variable}>
Source
@theme inline {
--font-sans: var(--font-brand), ui-sans-serif, system-ui, sans-serif;
}
  • Optical size: a face with an opsz axis (Inter, Roboto Flex, Fraunces) only gets it if you load it: Inter({ subsets: ["latin"], axes: ["opsz"] }). Keep font-optical-sizing: auto so 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 set size-adjust, ascent-override and descent-override on 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: antialiased on dark themes makes light text look less bloated on macOS. Leave light themes at the default.
  • Set ::selection to 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

    Free

    A 4px spacing scale, grouping by distance, alignment, containers, grids and responsive structure.

  • UX writing

    Free

    Headings, buttons, error messages, empty states and realistic sample content, all written without filler.

  • Color and dark mode

    Pro

    OKLCH role tokens, true neutral greys, one accent, both themes, and a script that checks contrast and gamut.