Comprehensive rules for building an accessible, fluid modular-scale typography system in modern web design systems.
You've been there: juggling a dozen random font sizes across your design system, watching text break on mobile, and fighting with designers over pixel-perfect spacing. Every responsive breakpoint becomes a maintenance nightmare, and accessibility audits keep flagging unreadable text.
Modern web apps demand typography that works everywhere—from 320px phones to 4K displays. But most developers are stuck with:
You need a typography system that scales mathematically, responds fluidly, and never breaks accessibility standards.
These Cursor Rules build fluid modular-scale typography systems that eliminate guesswork. Instead of managing dozens of arbitrary font sizes, you define one base size, one ratio, and let mathematics handle the rest.
The system generates semantic tokens (display-large, body-medium) that scale fluidly between breakpoints using CSS clamp(), ensuring perfect readability at every viewport while maintaining visual hierarchy.
clamp() scaling that responds to every pixel change/* Scattered across 20+ components */
.hero-title { font-size: 48px; }
.card-title { font-size: 24px; }
.body-text { font-size: 16px; }
/* Media query hell */
@media (max-width: 768px) {
.hero-title { font-size: 32px; }
.card-title { font-size: 20px; }
}
@media (max-width: 480px) {
.hero-title { font-size: 28px; }
/* Hope you remembered to update this... */
}
// One source of truth
$ratio: 1.25; // Major Third
$font-base: 1rem;
// Generated automatically
:root {
--font-size-display: clamp(2rem, 1.5rem + 2vw, 3rem);
--font-size-title: clamp(1.5rem, 1.2rem + 1vw, 2rem);
--font-size-body: 1rem; // Never goes below 16px
}
// Clean component styles
.hero-title { font-size: var(--font-size-display); }
.card-title { font-size: var(--font-size-title); }
.body-text { font-size: var(--font-size-body); }
Instead of this design-dev conversation:
"Make the heading bigger on mobile but not too big on desktop, and make sure it doesn't break the card layout..."
You get this:
"Use
title-largefor card headings." Done.
// tokens/_typography.scss
$ratio: 1.25; // Major Third - adjust for your brand
$font-base: 1rem; // 16px baseline
$scale: (
-2: $font-base / pow($ratio, 2), // 0.64rem (10px)
-1: $font-base / $ratio, // 0.8rem (13px)
0: $font-base, // 1rem (16px)
1: $font-base * $ratio, // 1.25rem (20px)
2: $font-base * pow($ratio, 2), // 1.56rem (25px)
3: $font-base * pow($ratio, 3), // 1.95rem (31px)
);
:root {
/* Semantic tokens that scale fluidly */
--font-size-body: 1rem;
--font-size-label: 0.875rem;
--font-size-title: clamp(1.25rem, 1rem + 1vw, 1.56rem);
--font-size-headline: clamp(1.56rem, 1.2rem + 1.5vw, 2.44rem);
--font-size-display: clamp(1.95rem, 1.5rem + 2vw, 3.05rem);
/* Line heights that adapt */
--line-height-body: 1.5;
--line-height-heading: clamp(1.1, 0.9 + 0.3vw, 1.3);
}
@mixin type($scale-name) {
font-size: var(--font-size-#{$scale-name});
line-height: var(--line-height-#{$scale-name});
max-inline-size: 65ch; // Optimal line length
}
.ts-display { @include type(display); }
.ts-headline { @include type(headline); }
.ts-title { @include type(title); }
.ts-body { @include type(body); }
// Typography validation tests
describe('Typography Tokens', () => {
test('body text meets minimum size requirements', () => {
expect(tokens.fontSize.body.min).toBeGreaterThanOrEqual(16);
});
test('line heights ensure readability', () => {
expect(tokens.lineHeight.body).toBeGreaterThanOrEqual(1.4);
expect(tokens.lineHeight.body).toBeLessThanOrEqual(1.6);
});
});
Your typography system becomes a competitive advantage: designers can focus on brand expression instead of pixel-pushing, developers ship features faster without typography headaches, and users get consistently readable experiences across every device.
The mathematics handle the complexity. You handle shipping great products.
You are an expert in CSS / SCSS, PostCSS, TypeScript-driven design-token pipelines, and modern responsive design techniques.
Key Principles
- Prioritise legibility: never render body copy below 1rem (16 px) and maintain 1.4 – 1.6 line-height.
- Design with semantic type tokens (display, headline, title, body, label) instead of hard-coded sizes.
- Build every size from a single base size using a mathematical ratio (e.g. Major Third 1.250) and expose the ratio as a token.
- Scale fluidly between two breakpoints with CSS clamp() so text responds smoothly to viewport changes.
- Cap line-length to 50 – 75 characters by constraining max-width instead of hard wraps.
- Separate content structure (HTML), visual style (CSS), and dynamic data (TS tokens) to keep the system maintainable.
CSS / SCSS Rules
- Declare all typography tokens once in a dedicated _typography.scss or tokens/typography.ts.
```scss
// tokens/_typography.scss
$ratio: 1.25; // Major Third
$font-base: 1rem; // 16px
$scale: (
-3: $font-base / pow($ratio,3),
-2: $font-base / pow($ratio,2),
-1: $font-base / $ratio,
0: $font-base,
1: $font-base * $ratio,
2: $font-base * pow($ratio,2),
3: $font-base * pow($ratio,3),
);
```
- Export custom properties at build-time using PostCSS or Style-Dictionary:
```css
:root {
--font-size-body : 1rem; /* 16px */
--font-size-title : clamp(1.5rem, 1.2rem + 1vw, 2rem);
--font-size-h1 : clamp(2rem , 1.5rem + 2vw, 3rem);
}
```
- Name custom properties with kebab-case nouns: `--font-size-title`, never abbreviations like `--fs-t`.
- Reference tokens, never literals, inside component styles: `font-size: var(--font-size-label);`.
- Use `em` for component-local overrides so they inherit fluidly from parents.
- Set `line-height` unit-less to inherit: `line-height: 1.5;`.
- For headings add tight leading at large sizes: `clamp(1.1, 0.9 + 0.3vw, 1.3)`.
- Restrict paragraph width: `max-inline-size: 65ch;`.
- Provide fallback font stacks: `font-family: "Inter", "Helvetica Neue", Arial, sans-serif;`.
Error Handling & Validation
- Guard against unreadable clamp outputs by testing min and max with Jest-style token tests:
```ts
expect(tokens.fontSize.body.min).toBeGreaterThanOrEqual(16);
expect(tokens.fontSize.body.max).toBeLessThanOrEqual(22);
```
- Lint with Stylelint rule `scale-unlimited/declaration-strict-value` to forbid direct pixel sizes.
- Fail CI if any size < 14 px or line-height < 1.2.
- Use Lighthouse a11y audits; block merge if font-size contrast warnings appear.
Design-System / Framework Rules (Material Design 3 inspired)
- Map semantic roles to scale steps:
• display-large → +3
• headline-medium→ +2
• title-large → +1
• body-medium → 0
• label-small → −1
- Provide utility classes generated from tokens: `.ts-display-lg { @include type(display-large); }`.
- In React, wrap tokens in a `TypographyProvider` that injects CSS custom properties at runtime for theming.
- Always expose a `prefers-reduced-motion` variant that removes fluid scaling (sets to max value) for users that disable dynamic UI.
Additional Sections
Testing
- Snapshot generated CSS tokens; diff on PR to catch unintended scale changes.
- Use Chromatic or Percy visual regression to verify that headings shrink/grow consistently across viewports (320 px–1920 px).
Performance
- Emit a single `:root` block; avoid per-component `clamp()` recalculations.
- Purge unused display sizes in production with Tailwind-like safelist.
Accessibility
- Check WCAG 2.2 SC 1.4.12: text can be resized up to 200 % without loss.
- Provide user-controlled font-size multiplier stored in `localStorage` and applied as `html { font-size: calc(100% * var(--user-scale,1)); }`.
Security
- Treat all design-token values as constants; forbid runtime string concatenation in inline styles to prevent injection.