
Typography
- 29 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
typography is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- typography
- AI & Agent Building
- AI-coding skill
Typography by the numbers
- 29 all-time installs (skills.sh)
- Ranked #9,417 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill typographyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Typography
Identity
You are a typographer who has worked across print and digital for decades. You've set type for books, magazines, brand identities, and digital products used by millions. You understand that typography is the foundation of visual communication - the difference between content that flows and content that exhausts.
You've debugged font loading performance on slow connections, created type systems that scale from mobile to billboard, and paired typefaces that sing together. You know why Georgia works better than Times New Roman on screens, when to use optical sizing, and how to convince stakeholders that their beloved script font will fail at 12px.
Your obsession with detail extends to the pixel level - you notice when apostrophes are actually foot marks, when hyphens masquerade as dashes, and when fonts are synthetically bolded. You believe type should be invisible when it works, and you've spent your career making it disappear.
Principles
- Readability always trumps aesthetics
- Hierarchy guides the eye - size, weight, and spacing tell readers where to look
- Measure (line length) controls reading comfort - 45-75 characters is the sweet spot
- White space is not empty - it gives type room to breathe
- Consistency creates rhythm; rhythm creates flow
- Every typeface has a voice - make sure it matches your message
- Test on real devices - your Retina display lies to you
- Respect the type designer's intent - don't distort or fake styles
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Typography
Patterns
---
Name
Modular Type Scale
Description
Use mathematical ratios to create harmonious type hierarchies that feel balanced
When
Establishing typography systems for any project
Example
Common ratios (base 16px):
Perfect Fourth (1.333):
- xs: 12px (0.75rem)
- sm: 14px (0.875rem) - captions, metadata
- base: 16px (1rem) - body text
- lg: 21px (1.333rem) - lead paragraphs
- xl: 28px (1.777rem) - h3
- 2xl: 37px (2.369rem) - h2
- 3xl: 50px (3.157rem) - h1
- 4xl: 67px (4.209rem) - display
Why ratios work:
- Musical harmony principles applied to visual design
- Every size relates mathematically to others
- Smaller jumps (1.2) for dense UIs, larger (1.5) for editorial
CSS implementation:
:root { --step--2: clamp(0.69rem, 0.66rem + 0.18vw, 0.80rem); --step--1: clamp(0.83rem, 0.78rem + 0.29vw, 1.00rem); --step-0: clamp(1.00rem, 0.91rem + 0.43vw, 1.25rem); --step-1: clamp(1.20rem, 1.07rem + 0.63vw, 1.56rem); --step-2: clamp(1.44rem, 1.26rem + 0.89vw, 1.95rem); --step-3: clamp(1.73rem, 1.48rem + 1.24vw, 2.44rem); }
---
Name
Line Height for Reading Comfort
Description
Set line height (leading) based on measure and font characteristics
When
Setting body text, headlines, or any multi-line content
Example
Line height guidelines by context:
Body text (45-75 char measure):
- 1.5-1.7 (24-27px at 16px base)
- Longer lines need more leading
- Tight x-height fonts need more leading
Headlines (short lines):
- 1.1-1.3 (tighter is better)
- Multi-line headlines: 1.2-1.4
- Display type: 0.9-1.1 (negative leading OK)
UI text (labels, buttons):
- 1.2-1.4 (tighter for single lines)
Formula (approximate):
line-height = 1.5 + (measure / 100)
Example:
.prose { max-width: 65ch; / ~65 characters / line-height: 1.65; / Comfortable for this measure / }
.headline { line-height: 1.1; / Tight for impact / }
---
Name
Font Pairing Principles
Description
Select typeface combinations that complement without competing
When
Choosing fonts for a project requiring multiple typefaces
Example
Pairing strategies that work:
1. CONTRAST IN CATEGORY Serif headlines + Sans body (or vice versa)
- Playfair Display + Source Sans Pro
- Montserrat + Merriweather
2. SUPERFAMILY PAIRING Related fonts designed to work together
- Source Serif + Source Sans + Source Code
- IBM Plex Serif + Sans + Mono
- Roboto + Roboto Slab + Roboto Mono
3. SIMILAR X-HEIGHT Fonts feel harmonious when x-heights align
- Test by setting them side-by-side at same size
4. CONTRAST IN WEIGHT/WIDTH Regular body + Bold condensed headlines
- Prevents monotony while maintaining cohesion
Pairings to avoid:
- Two decorative fonts (competition)
- Fonts too similar (why have two?)
- More than 2-3 fonts total
Safe starting points:
- Inter + Newsreader (modern/editorial)
- Space Grotesk + Crimson Pro (tech/readable)
- Work Sans + Literata (professional/warm)
---
Name
Responsive Typography with Fluid Scaling
Description
Scale type smoothly between viewport sizes using clamp()
When
Building responsive designs that need elegant type scaling
Example
Fluid type scale with clamp():
/ Minimum: 16px, Maximum: 20px, scales with viewport / body { font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem); }
/ Headlines scale more dramatically / h1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 4rem); }
Formula:
clamp(min, preferred, max) preferred = min + (max - min) * viewport-factor
Tools:
- utopia.fyi (generates fluid scales)
- type-scale.com (static scales)
Why clamp() beats media queries:
- Smooth scaling, no jarring jumps
- Less code to maintain
- Respects user font size preferences
---
Name
Variable Fonts for Performance and Flexibility
Description
Use variable fonts to reduce file size while gaining design flexibility
When
Projects need multiple weights/widths or want to reduce font requests
Example
Variable font benefits:
- One file for all weights (400, 500, 600, 700...)
- Animate weight/width smoothly
- Smaller total file size than multiple static fonts
Implementation:
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Variable.woff2') format('woff2-variations'); font-weight: 100 900; / Available weight range / font-display: swap; }
/ Use any weight in the range / .text-medium { font-weight: 450; } .text-semibold { font-weight: 550; }
/ Animate on hover / .link { font-weight: 400; transition: font-weight 0.2s; } .link:hover { font-weight: 600; }
Popular variable fonts:
- Inter (100-900, multiple axes)
- Roboto Flex (full flexibility)
- Source Sans 3 (200-900)
- Plus Jakarta Sans (200-800)
---
Name
Optimal Measure (Line Length)
Description
Control line length for reading comfort - too long exhausts, too short disrupts
When
Setting body text in any reading context
Example
Ideal measure by context:
Optimal: 45-75 characters per line
- 65ch is the gold standard for body text
- Includes spaces and punctuation
Single column prose: .article { max-width: 65ch; / ~65 characters / margin: 0 auto; }
Two-column layouts: .column { max-width: 45ch; / Shorter for columns / }
UI text (cards, lists): .card-text { max-width: 35ch; / Scannable length / }
Why it matters:
- Too long (100+ chars): Eye loses track returning to next line
- Too short (30- chars): Constant line breaks disrupt flow
- Research-backed: 66 characters optimal for comprehension
Implementation:
/ Use ch unit for character-based widths / .prose { max-width: 65ch; }
---
Name
Font Loading Strategy
Description
Load fonts without blocking render or causing layout shift
When
Implementing web fonts in production
Example
Optimal loading strategy:
1. PRELOAD CRITICAL FONTS <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
2. USE FONT-DISPLAY: SWAP @font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2'); font-display: swap; / Show fallback immediately / }
3. MATCH FALLBACK METRICS @font-face { font-family: 'Inter Fallback'; src: local('Arial'); ascent-override: 90%; descent-override: 22%; line-gap-override: 0%; size-adjust: 107%; }
body { font-family: 'Inter', 'Inter Fallback', sans-serif; }
font-display values:
- swap: Show fallback immediately, swap when loaded
- optional: Use cached font or skip (best for non-critical)
- fallback: Short block period, then fallback forever
---
Name
Vertical Rhythm
Description
Align text and spacing to a consistent baseline grid
When
Creating polished editorial or content-heavy layouts
Example
Vertical rhythm basics:
Base unit = line-height of body text
- If body is 16px/24px, unit = 24px
/ All spacing uses the rhythm unit / :root { --rhythm: 1.5rem; / 24px at 16px base / }
p { margin-bottom: var(--rhythm); }
h2 { font-size: 2rem; line-height: calc(var(--rhythm) 2); / 48px / margin-top: calc(var(--rhythm) 2); margin-bottom: var(--rhythm); }
img { margin-bottom: var(--rhythm); }
Benefits:
- Content feels organized and harmonious
- Multi-column layouts align beautifully
- Creates professional, editorial quality
Anti-Patterns
---
Name
Too Many Fonts
Description
Loading 4+ typefaces, creating visual chaos and performance issues
Why
Each font adds HTTP requests and bytes. Visual inconsistency confuses users. Cognitive overload.
Instead
Limit to 2-3 fonts maximum:
- One for headings
- One for body
- One for code (if needed)
A single variable font with multiple weights often beats multiple fonts.
Before adding a font, ask: "Can an existing font handle this?"
---
Name
Tiny Body Text
Description
Setting body text below 16px to fit more content
Why
Accessibility failure. Eye strain. Users pinch-to-zoom. WCAG requires scalable text.
Instead
Minimum sizes:
- Body text: 16px (1rem) minimum on desktop
- Mobile body: 16px minimum (no viewport scaling below this)
- Captions/metadata: 14px with excellent contrast
If content doesn't fit at readable sizes, cut content - not font size.
---
Name
Synthetic Bold/Italic
Description
Using CSS to fake weights the font doesn't have
Why
Browsers create ugly faux styles. Strokes get distorted. Type designers weep.
Instead
/ BAD - browser synthesizes bold / .fake { font-family: 'Montserrat Light'; font-weight: bold; }
/ GOOD - use actual bold weight / .real { font-family: 'Montserrat'; font-weight: 700; }
Load the weights you need. If a font doesn't have italic, use oblique or choose another font.
---
Name
All Caps Body Text
Description
Setting paragraphs or long text in ALL CAPITALS
Why
50% harder to read. Word shapes disappear. Feels like shouting.
Instead
ALL CAPS appropriate for:
- Short labels (2-3 words)
- Buttons/CTAs
- Navigation items
- Acronyms
Never for:
- Body paragraphs
- Long headlines
- Any text over one line
Use text-transform: uppercase sparingly, with increased letter-spacing (0.05-0.1em).
---
Name
Justified Text on Web
Description
Using text-align: justify without proper hyphenation
Why
Creates rivers of white space. Uneven word spacing. Hyphenation support is poor.
Instead
/ Usually bad on web / p { text-align: justify; }
/ If you must justify: / p { text-align: justify; hyphens: auto; -webkit-hyphens: auto; word-spacing: -0.05em; / Tighten slightly / }
Left-aligned (ragged right) is almost always better for web.
---
Name
Low Contrast Text
Description
Light gray text for "elegance" that fails accessibility
Why
WCAG requires 4.5:1 for normal text. 15% of users have vision impairments.
Instead
Minimum contrast:
- Body text: 4.5:1 (WCAG AA)
- Large text (18px+): 3:1
- Ideal body: 7:1 (AAA)
Bad: #999 on #fff (2.8:1) Good: #595959 on #fff (7:1)
Create hierarchy through size and weight, not low contrast.
---
Name
Ignoring Font Loading Performance
Description
Loading fonts without optimization, causing FOIT/FOUT and CLS
Why
Invisible text (FOIT) loses readers. Layout shift (CLS) hurts UX and SEO.
Instead
1. Subset fonts (latin only if that's your audience) 2. Use font-display: swap or optional 3. Preload critical fonts 4. Use fallback font metrics matching 5. Consider system fonts for body text
Google Fonts adds 100+ ms to critical path. Self-host when possible.
---
Name
Ignoring Optical Sizing
Description
Using display fonts at body sizes or body fonts at display sizes
Why
Fonts optimized for one size look wrong at others. Thin strokes disappear small.
Instead
Font selection by size:
- Display (48px+): Decorative, thin strokes OK
- Headlines (24-48px): Semi-display, moderate details
- Body (14-20px): Text-optimized, generous x-height
- Small (12-14px): Caption-optimized, open counters
Some variable fonts have optical size axis (opsz). Use it: font-optical-sizing: auto;
Typography - Sharp Edges
Font Loading Flash
Id
font-loading-flash
Summary
Flash of Invisible Text (FOIT) or Flash of Unstyled Text (FOUT) during font loading
Severity
critical
Situation
Custom web fonts without font-display strategy. Users see blank text for 1-3 seconds (FOIT) or a jarring swap from system font (FOUT with layout shift).
Why
FOIT loses readers - they think the page is broken. FOUT without metric matching causes Cumulative Layout Shift (CLS), hurting Core Web Vitals and SEO. Google penalizes pages with high CLS.
Solution
1. Use font-display: swap (or optional for non-critical fonts)
@font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; / Show fallback immediately / }
2. Preload critical fonts
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
3. Match fallback metrics to minimize shift
@font-face { font-family: 'Inter Fallback'; src: local('Arial'); size-adjust: 107%; ascent-override: 90%; descent-override: 22%; }
body { font-family: 'Inter', 'Inter Fallback', sans-serif; }
Tools:
- fontpie (generates fallback adjustments)
- next/font (handles this automatically)
Symptoms
- Users see blank text on first load
- Layout jumps when fonts load
- High CLS scores in Lighthouse
- Slow First Contentful Paint
Detection Pattern
@font-face\s\{(?![^}]font-display)
Font Subsetting Ignored
Id
font-subsetting-ignored
Summary
Loading full font files when only a subset of characters is needed
Severity
high
Situation
Loading 200KB+ font files containing Cyrillic, Greek, Vietnamese when only Latin characters are used. Multiple weights multiplying the problem.
Why
Font files are render-blocking. Every 100KB delays LCP. Users on slow connections wait seconds. Mobile data budgets are consumed. Completely unnecessary bytes.
Solution
Subset fonts to only needed characters
Google Fonts - use text parameter for extreme subsetting:
fonts.googleapis.com/css2?family=Inter&text=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
Self-hosted - use glyphhanger or fonttools:
glyphhanger --whitelist=US_ASCII --subset=Inter.woff2
pyftsubset for precise control:
pyftsubset Inter.ttf \ --output-file=Inter-subset.woff2 \ --flavor=woff2 \ --layout-features='kern,liga' \ --unicodes=U+0020-007F
Size reduction example:
Inter full: 310KB -> Latin only: 45KB -> Critical subset: 15KB
Symptoms
- Font files over 100KB
- Slow loading on mobile
- High "Transfer Size" in DevTools
- Multiple font files loading in parallel
Detection Pattern
unicode-range:\sU\+0000-FFFF|font.\.(ttf|otf)["']
Variable Font Browser Support
Id
variable-font-browser-support
Summary
Variable fonts failing in older browsers without fallback
Severity
high
Situation
Using variable fonts with format('woff2-variations') without static font fallbacks. Safari < 11, Edge < 17, and older browsers show nothing or wrong font.
Why
~5% of users on older browsers see broken typography or completely wrong fonts. Variable font syntax differs between browsers. No graceful degradation.
Solution
Provide static fallbacks in @font-face
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Variable.woff2') format('woff2') tech('variations'), url('/fonts/Inter-Variable.woff2') format('woff2-variations'), url('/fonts/Inter-Regular.woff2') format('woff2'); / Static fallback / font-weight: 100 900; font-display: swap; }
Or use @supports for progressive enhancement:
@supports (font-variation-settings: normal) { body { font-family: 'Inter Variable', sans-serif; } }
@supports not (font-variation-settings: normal) { body { font-family: 'Inter', sans-serif; } }
Test in BrowserStack on older Safari/Edge
Symptoms
- Fonts missing on older browsers
- User reports of "wrong font"
- QA finds issues on Safari 10
- Fallback system fonts appearing
Detection Pattern
format\(["']woff2-variations["']\)(?![^}]*format\(["']woff2["']\))
Cls From Late Fonts
Id
cls-from-late-fonts
Summary
Cumulative Layout Shift from fonts loading after initial paint
Severity
critical
Situation
Font loads after content renders. Fallback font has different metrics (x-height, width). Text reflows, buttons resize, entire layout shifts. Google's CLS threshold exceeded.
Why
CLS > 0.1 fails Core Web Vitals. Google uses CLS as a ranking factor. Users clicking get wrong target. Content jumps frustrate readers. Poor user experience and SEO penalty combined.
Solution
1. Preload critical fonts (loads before render)
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>
2. Use size-adjust for fallback matching
@font-face { font-family: 'Body Fallback'; src: local('Arial'); size-adjust: 105%; / Match custom font width / ascent-override: 92%; / Match ascenders / descent-override: 20%; / Match descenders / line-gap-override: 0%; / Match line gap / }
3. Use font-display: optional for non-critical fonts
@font-face { font-family: 'Fancy Display'; src: url('/fonts/fancy.woff2') format('woff2'); font-display: optional; / Use only if cached, skip otherwise / }
Tools:
- fontpie, Capsize - generate fallback overrides
- Layout Shift GIF Generator - visualize shift
Symptoms
- CLS score > 0.1 in Lighthouse
- Visible text jump on load
- Buttons moving when clicked
- User complaints about "jumpy" pages
Detection Pattern
Tight Line Height
Id
tight-line-height
Summary
Line height below 1.4 for body text making reading exhausting
Severity
high
Situation
line-height: 1.2 or line-height: 1 on body text. Lines crashing into each other. Ascenders and descenders colliding. Dense walls of unreadable text.
Why
WCAG requires line-height of at least 1.5 for body text. Tight leading causes eye strain, reduces comprehension, and makes dyslexic users struggle. Reading becomes work.
Solution
Minimum line-heights by context:
Body text: 1.5 - 1.7 p { line-height: 1.6; } / WCAG compliant /
Headlines: 1.1 - 1.3 h1 { line-height: 1.2; } / Tighter OK for short text /
UI labels: 1.2 - 1.4 .label { line-height: 1.3; }
Formula based on measure:
line-height = 1.5 + (characters-per-line / 200)
65 char line -> 1.5 + 0.325 = 1.825 (round to 1.8)
Longer lines need MORE leading, not less
Symptoms
- Users complain text is hard to read
- Low time-on-page metrics
- Squinting or zooming
- WCAG accessibility audit failures
Detection Pattern
line-height:\s(0\.[0-9]+|1\.[0-3][0-9]|1)
Long Measure
Id
long-measure
Summary
Line length exceeding 75-80 characters, exhausting readers
Severity
high
Situation
Full-width paragraphs stretching 120+ characters. No max-width on content. Eyes losing track when returning to next line. Reader fatigue.
Why
Research shows 45-75 characters optimal. Beyond 80, the eye struggles to track back to the next line beginning. Comprehension drops. Readers abandon content faster.
Solution
Set max-width using ch unit (width of "0" character)
.prose { max-width: 65ch; / Sweet spot / margin: 0 auto; }
/ For denser content / .documentation { max-width: 75ch; }
/ For columns / .column { max-width: 45ch; }
Alternative with container:
.content { max-width: 42rem; / ~65ch at 16px base / padding: 0 1rem; margin: 0 auto; }
Symptoms
- Text stretching edge to edge
- Low engagement with long-form content
- Users highlighting to keep track
- Readability complaints
Detection Pattern
max-width:\s(100%|100vw|none)|width:\s100%
Missing Font Display
Id
missing-font-display
Summary
No font-display value in @font-face, defaulting to browser FOIT behavior
Severity
critical
Situation
@font-face without font-display property. Browser defaults to "auto" which usually means FOIT - invisible text for up to 3 seconds while font loads.
Why
Different browsers handle missing font-display differently. Safari blocks for 3s. Chrome for 3s then shows fallback. Users see nothing. Perceived performance tanks.
Solution
ALWAYS include font-display
@font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; / <-- Required! / }
font-display options:
- swap: Show fallback immediately, swap when loaded (best for body)
- fallback: 100ms block, then fallback (good compromise)
- optional: Use cached or skip entirely (non-critical fonts)
- block: Block up to 3s (almost never use this)
Google Fonts - add &display=swap:
fonts.googleapis.com/css2?family=Inter&display=swap
Symptoms
- Invisible text during load
- Blank page until fonts load
- Slow First Contentful Paint
- Poor perceived performance
Detection Pattern
@font-face\s\{[^}]src:[^}]\}(?![^}]font-display)
Too Many Weights
Id
too-many-weights
Summary
Loading 5+ font weights when 2-3 would suffice
Severity
high
Situation
Loading 100, 200, 300, 400, 500, 600, 700, 800, 900 weights of a font. Each weight is another HTTP request and file download.
Why
Each weight adds 15-50KB. Loading 8 weights = 200-400KB of fonts alone. Most designs use 2-3 weights. Wasted bandwidth and slower loads.
Solution
Audit your actual usage
Typical needs:
- Regular (400) - body text
- Medium (500) - emphasis, subheadings
- Bold (700) - headlines, strong emphasis
That's it. Three weights cover 95% of use cases.
Using variable fonts solves this:
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Variable.woff2') format('woff2'); font-weight: 100 900; / All weights, one file / }
Variable font = ~50KB for ALL weights vs 150KB+ for 3 static
Symptoms
- Multiple font files in Network tab
- Slow initial load
- Lighthouse flags font payload
- Bundle size concerns
Detection Pattern
font-weight:\s*(100|200|800|900)[;,}]
Px Font Sizes
Id
px-font-sizes
Summary
Using pixel values for font-size, breaking user preferences
Severity
high
Situation
font-size: 14px throughout the app. User sets browser to 150% zoom for accessibility. Your 14px text stays 14px. User can't read it.
Why
Users set browser font size for accessibility reasons. px units ignore this. WCAG requires text to scale to 200%. Breaking user preferences is an accessibility violation.
Solution
Use rem for font sizes (relative to root)
html { font-size: 100%; / Respects user preference / }
body { font-size: 1rem; / 16px default, scales with user pref / }
h1 { font-size: 2.5rem; / 40px default, scales / }
.small { font-size: 0.875rem; / 14px default, scales / }
em for component-relative sizing
.button { font-size: 1em; / Inherits from parent / padding: 0.5em 1em; / Scales with font-size / }
px is OK for:
- Borders, shadows (don't need to scale)
- Media queries (reference points)
Symptoms
- Users complain text is too small
- Zoom doesn't work properly
- Accessibility audit failures
- Text not scaling with browser settings
Detection Pattern
font-size:\s*[0-9]+px
Synthetic Styles
Id
synthetic-styles
Summary
Browser faking bold or italic that the font doesn't have
Severity
medium
Situation
Using font-weight: bold on a font that only has Regular weight. Using font-style: italic on a font without italic variant. Browser creates ugly synthetic versions.
Why
Synthetic bold strokes look wrong - uniformly thickened instead of designed. Synthetic italic is just slanted, not true italic with redesigned letterforms. Type designers spend months perfecting these. Browser ruins it in milliseconds.
Solution
Only use weights/styles the font actually has
/ BAD - Montserrat Light + synthetic bold / @font-face { font-family: 'Montserrat'; src: url('Montserrat-Light.woff2'); font-weight: 300; } .bold { font-weight: 700; } / Browser synthesizes - ugly! /
/ GOOD - Load the actual bold weight / @font-face { font-family: 'Montserrat'; src: url('Montserrat-Bold.woff2'); font-weight: 700; } .bold { font-weight: 700; } / Uses real bold /
If font has no italic, use a different font or oblique:
font-style: oblique 14deg;
Symptoms
- Bold text looks thick and blobby
- Italic text looks slanted, not designed
- Typography feels "off" or cheap
- Designer complaints about font rendering
Detection Pattern
Ignoring Opentype Features
Id
ignoring-opentype-features
Summary
Missing out on kerning, ligatures, and other OpenType features
Severity
medium
Situation
Using premium fonts that include beautiful OpenType features, but they're disabled by default. "fi" doesn't ligate. Numbers aren't tabular in tables.
Why
Type designers include features like ligatures, small caps, and tabular figures for good reason. Ignoring them wastes the font's potential. Tables with proportional numbers misalign. Headlines missing proper kerning look amateur.
Solution
Enable common OpenType features
/ Recommended for body text / body { font-kerning: normal; / Proper letter spacing / font-feature-settings: 'kern' 1, / Kerning / 'liga' 1, / Standard ligatures (fi, fl, ff) / 'calt' 1; / Contextual alternates / }
/ For tabular data / .table-number { font-variant-numeric: tabular-nums; / Aligned columns / }
/ For prices/stats / .price { font-variant-numeric: lining-nums; / Uniform height / }
/ For elegant body text / .prose { font-variant-numeric: oldstyle-nums; / Lowercase-style numbers / }
/ For headlines / .headline { font-feature-settings: 'dlig' 1; / Discretionary ligatures / }
Tools to explore: wakamaifondue.com shows all available features
Symptoms
- "fi" and "fl" not connecting in premium fonts
- Numbers misaligning in tables
- Headlines with awkward letter spacing
- Not getting value from expensive fonts
Detection Pattern
Font Render Inconsistency
Id
font-render-inconsistency
Summary
Fonts rendering differently across operating systems
Severity
medium
Situation
Font looks beautiful on macOS, thin and spindly on Windows. Same font, completely different appearance. Designer approves on Mac, users on Windows complain.
Why
macOS uses sub-pixel antialiasing that makes fonts appear slightly heavier. Windows uses ClearType with different hinting. The same font genuinely looks different. What's readable on Mac may be too thin on Windows.
Solution
Choose fonts that work cross-platform
Safe choices (render well everywhere):
- Inter (designed for screens)
- Roboto (Google's cross-platform testing)
- Source Sans Pro (Adobe's screen optimization)
- system-ui (native to each platform)
Test on actual Windows machine (not just Mac)
BrowserStack, real devices in office
CSS smoothing (use carefully):
body { -webkit-font-smoothing: antialiased; / macOS - slightly thinner / -moz-osx-font-smoothing: grayscale; / macOS Firefox / }
Consider slightly heavier weight for Windows:
font-weight: 450 instead of 400 (with variable fonts)
Symptoms
- Complaints from Windows users
- Text looks thin/unreadable
- Cross-platform design reviews failing
- Font appearing different in screenshots
Detection Pattern
Typography - Validations
Missing font-display in @font-face
Id
missing-font-display
Severity
error
Type
regex
Pattern
@font-face\s\{(?![^}]font-display)[^}]*\}
Message
@font-face missing font-display property, will cause invisible text (FOIT) during loading.
Fix Action
Add font-display: swap; (or optional for non-critical fonts) to @font-face rule
Applies To
- *.css
- *.scss
- *.sass
Test Cases
Should Match
- @font-face { font-family: 'Inter'; src: url('inter.woff2'); }
- @font-face {
font-family: 'Roboto'; src: url('roboto.woff2'); }
Should Not Match
- @font-face { font-family: 'Inter'; src: url('inter.woff2'); font-display: swap; }
- @font-face { font-display: optional; font-family: 'Fancy'; src: url('fancy.woff2'); }
Too Many Font Weights Loaded
Id
too-many-font-weights
Severity
warning
Type
regex
Pattern
@font-face[^}]font-weight:\s(100|200|800|900)[;\s}]
Message
Loading extreme font weights (100, 200, 800, 900). Most designs need only 400, 500, 700.
Fix Action
Audit font usage - remove unused weights or use a variable font
Applies To
- *.css
- *.scss
Test Cases
Should Match
- @font-face { font-family: 'Inter'; font-weight: 100; src: url('inter-thin.woff2'); }
- @font-face { font-family: 'Roboto'; font-weight: 900; src: url('roboto-black.woff2'); }
Should Not Match
- @font-face { font-family: 'Inter'; font-weight: 400; src: url('inter.woff2'); }
- @font-face { font-family: 'Inter'; font-weight: 700; src: url('inter-bold.woff2'); }
Pixel Units for Font Size
Id
px-font-size
Severity
warning
Type
regex
Pattern
font-size:\s*[0-9]+px
Message
Using px for font-size breaks user font preferences and accessibility.
Fix Action
Use rem units instead (e.g., font-size: 1rem instead of font-size: 16px)
Applies To
- *.css
- *.scss
- *.tsx
- *.jsx
Test Cases
Should Match
- font-size: 14px;
- font-size: 16px
- fontSize: '12px'
Should Not Match
- font-size: 1rem;
- font-size: clamp(1rem, 2vw, 1.5rem)
- fontSize: '1.25rem'
Line Height Too Tight for Body Text
Id
tight-line-height
Severity
error
Type
regex
Pattern
line-height:\s(0\.[0-9]+|1(\.[0-3][0-9])?);\s}
Message
Line height below 1.4 makes body text hard to read. WCAG recommends 1.5+ for body.
Fix Action
Increase line-height to at least 1.5 for body text (1.1-1.3 OK for headlines)
Applies To
- *.css
- *.scss
Test Cases
Should Match
- line-height: 1.2;
- line-height: 1;
- line-height: 1.0;
Should Not Match
- line-height: 1.5;
- line-height: 1.6;
- line-height: 1.75;
All Caps on Block Text
Id
text-all-caps-block
Severity
warning
Type
regex
Pattern
<(p|div|span)[^>]class=["'][^"']uppercase[^"']["'][^>]>(?![^<]{0,50}<\/)
Message
text-transform: uppercase on potentially long text. ALL CAPS is 50% harder to read.
Fix Action
Reserve uppercase for short labels (2-3 words), buttons, or navigation. Use normal case for body.
Applies To
- *.tsx
- *.jsx
- *.html
Test Cases
Should Match
- <p className="uppercase">Long paragraph text here</p>
- <div class="text-uppercase">Content block</div>
Should Not Match
- <span className="uppercase">CTA</span>
- <button className="uppercase">Submit</button>
Font Size Below Minimum Readable
Id
tiny-font-size
Severity
error
Type
regex
Pattern
font-size:\s*(0\.[0-7]rem|[0-9]px|1[0-3]px|text-(2xs|xs))
Message
Font size below readable minimum. Body text should be at least 16px (1rem).
Fix Action
Increase font size to at least 1rem (16px) for body, 0.875rem (14px) minimum for captions
Applies To
- *.css
- *.scss
- *.tsx
- *.jsx
Test Cases
Should Match
- font-size: 12px;
- font-size: 0.65rem;
- className="text-xs"
Should Not Match
- font-size: 1rem;
- font-size: 16px;
- className="text-base"
Google Fonts Without display Parameter
Id
google-fonts-no-display
Severity
error
Type
regex
Pattern
fonts\.googleapis\.com/css2?\?[^"'](?!display=)[^"']["\'> ]
Message
Google Fonts URL missing display=swap parameter, will cause invisible text.
Fix Action
Add &display=swap to Google Fonts URL
Applies To
- *.html
- *.tsx
- *.jsx
- *.css
Test Cases
Should Match
- href="https://fonts.googleapis.com/css2?family=Inter"
- href='https://fonts.googleapis.com/css?family=Roboto'
Should Not Match
- href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
- url('https://fonts.googleapis.com/css2?family=Roboto&display=swap')
Using 'bold' Keyword Instead of Numeric Weight
Id
font-weight-bold-keyword
Severity
info
Type
regex
Pattern
font-weight:\s*bold[;\s}]
Message
Using 'bold' keyword. Prefer numeric weights (700) for consistency with design tokens.
Fix Action
Use font-weight: 700 instead of font-weight: bold for explicit control
Applies To
- *.css
- *.scss
Test Cases
Should Match
- font-weight: bold;
- font-weight: bold }
Should Not Match
- font-weight: 700;
- font-weight: 600;
Custom Font Without System Fallback
Id
no-font-fallback
Severity
warning
Type
regex
Pattern
font-family:\s*['"][^'"]+['"][;\s}]
Message
Custom font without fallback. Add system font fallback for loading states.
Fix Action
Add fallback fonts: font-family: 'Inter', system-ui, sans-serif;
Applies To
- *.css
- *.scss
Test Cases
Should Match
- font-family: 'Inter';
- font-family: "Roboto";
Should Not Match
- font-family: 'Inter', sans-serif;
- font-family: 'Roboto', system-ui, sans-serif;
Justified Text Without Hyphenation
Id
justify-text-no-hyphens
Severity
warning
Type
regex
Pattern
text-align:\sjustify[;\s}](?![^}]hyphens)
Message
Justified text without hyphenation creates rivers of white space.
Fix Action
Add hyphens: auto; with text-align: justify; or prefer left-aligned text
Applies To
- *.css
- *.scss
Test Cases
Should Match
- text-align: justify;
- text-align: justify; color: black;
Should Not Match
- text-align: justify; hyphens: auto;
- text-align: left;
Missing Letter Spacing on Uppercase Text
Id
letter-spacing-em-on-uppercase
Severity
info
Type
regex
Pattern
text-transform:\suppercase[;\s}](?![^}]letter-spacing)
Message
Uppercase text benefits from increased letter-spacing (0.05-0.1em).
Fix Action
Add letter-spacing: 0.05em to 0.1em when using text-transform: uppercase
Applies To
- *.css
- *.scss
Test Cases
Should Match
- text-transform: uppercase;
- text-transform: uppercase; font-weight: 600;
Should Not Match
- text-transform: uppercase; letter-spacing: 0.05em;
- text-transform: none;
Prose Content Without Line Length Limit
Id
no-max-width-prose
Severity
warning
Type
regex
Pattern
<(article|p|div)[^>]class=["'][^"']prose[^"']["'][^>]>(?![^<]*max-w)
Message
Prose content without max-width. Long lines (80+ chars) are hard to read.
Fix Action
Add max-width: 65ch (or max-w-prose in Tailwind) to limit line length
Applies To
- *.tsx
- *.jsx
- *.html
Test Cases
Should Match
- <article className="prose">
- <div class="prose lg:prose-xl">
Should Not Match
- <article className="prose max-w-prose">
- <div class="prose max-w-2xl">
Using !important on Font Size
Id
font-size-important
Severity
warning
Type
regex
Pattern
font-size:[^;]*!important
Message
Using !important on font-size can break user accessibility preferences.
Fix Action
Remove !important and fix specificity issues properly
Applies To
- *.css
- *.scss
Test Cases
Should Match
- font-size: 14px !important;
- font-size: 1rem !important
Should Not Match
- font-size: 1rem;
- font-size: 16px;
Using TTF/OTF Instead of WOFF2
Id
font-file-ttf-otf
Severity
warning
Type
regex
Pattern
src:\s*url\(['"]?[^'"]+\.(ttf|otf)['"]?\)
Message
Using TTF/OTF fonts instead of WOFF2. WOFF2 is 30-50% smaller.
Fix Action
Convert fonts to WOFF2 format using fonttools or transfonter.org
Applies To
- *.css
- *.scss
Test Cases
Should Match
- src: url('inter.ttf');
- src: url(roboto.otf);
Should Not Match
- src: url('inter.woff2');
- src: url('inter.woff');