Tutorial

Responsive Typography with Design Tokens

Build fluid, responsive typography systems with design tokens and CSS clamp(). Scale perfectly across devices without breakpoint hell.

FramingUI Team18 min read

The Typography Problem

You're building a hero section. The heading looks perfect on desktop (48px), but on mobile it's way too big. You add a media query:

h1 {
  font-size: 48px;
}

@media (max-width: 768px) {
  h1 {
    font-size: 32px;
  }
}

Then your designer asks for a tablet size. And a small desktop size. And a large desktop size. Soon you have:

h1 {
  font-size: 32px; /* Mobile */
}

@media (min-width: 640px) {
  h1 {
    font-size: 36px; /* Tablet portrait */
  }
}

@media (min-width: 768px) {
  h1 {
    font-size: 40px; /* Tablet landscape */
  }
}

@media (min-width: 1024px) {
  h1 {
    font-size: 48px; /* Desktop */
  }
}

@media (min-width: 1536px) {
  h1 {
    font-size: 56px; /* Large desktop */
  }
}

Five breakpoints. Five font sizes. One heading.

Now multiply this by every text element in your design system (headings, body text, captions, labels). You end up with hundreds of media queries and a maintenance nightmare.

There's a better way: fluid typography with clamp() + design tokens.

TL;DR

  • Problem: Traditional responsive typography requires multiple breakpoints and media queries
  • Solution: CSS clamp() creates fluid scaling between min/max sizes
  • Formula: font-size: clamp(minSize, preferredSize, maxSize)
  • Design tokens store fluid type scales centrally and make them reusable
  • Benefits: Fewer breakpoints, smoother scaling, easier maintenance
  • AI-friendly: Tokens with clamp() values help AI generate responsive typography automatically
  • Best practice: Define fluid type scales as tokens, generate CSS variables + Tailwind utilities

What is Fluid Typography?

Fluid typography means font sizes scale smoothly with viewport width, without discrete breakpoints.

Traditional (Stepped)

Mobile    Tablet    Desktop
 32px  →  40px  →   48px
  |         |         |
 640px    1024px   1536px

Typography "jumps" at each breakpoint.

Fluid (Smooth)

Mobile → ········ → Desktop
 32px   gradually    48px
  |     increases     |
 640px              1536px

Typography scales continuously between min and max.

CSS clamp() Explained

The clamp() function takes three values:

font-size: clamp(MIN, PREFERRED, MAX);
  • MIN: Minimum size (e.g., 32px) — never smaller than this
  • PREFERRED: Ideal size using viewport units (e.g., 4vw) — scales with screen width
  • MAX: Maximum size (e.g., 48px) — never larger than this

Example:

h1 {
  font-size: clamp(32px, 4vw, 48px);
}

How it works:

Viewport WidthCalculationActual Size
400px4vw = 16px32px (clamped to min)
800px4vw = 32px32px
1000px4vw = 40px40px
1200px4vw = 48px48px (clamped to max)
1600px4vw = 64px48px (clamped to max)

Between 800px and 1200px, the heading scales smoothly. Outside that range, it clamps to min/max.

Visual Comparison

Traditional breakpoints:

Size
 ▲
48│                     ┌─────────
   │                     │
40│              ┌──────┘
   │              │
32│──────────────┘
   └──────────────────────────────► Viewport
     640px      1024px      1536px

Typography steps at breakpoints—visible jumps when resizing.

Fluid typography:

Size
 ▲
48│                    ╱────────────
   │                 ╱
40│              ╱
   │           ╱
32│────────╱
   └──────────────────────────────► Viewport
     640px      1024px      1536px

Typography scales smoothly—no visible jumps.

Building a Fluid Type Scale with Tokens

Step 1: Define Base Sizes

Start with min/max sizes for each heading level:

{
  "typography": {
    "heading": {
      "h1": {
        "min": { "value": "32px", "type": "dimension" },
        "max": { "value": "48px", "type": "dimension" }
      },
      "h2": {
        "min": { "value": "28px", "type": "dimension" },
        "max": { "value": "40px", "type": "dimension" }
      },
      "h3": {
        "min": { "value": "24px", "type": "dimension" },
        "max": { "value": "32px", "type": "dimension" }
      }
    },
    "body": {
      "lg": {
        "min": { "value": "16px", "type": "dimension" },
        "max": { "value": "18px", "type": "dimension" }
      },
      "base": {
        "min": { "value": "14px", "type": "dimension" },
        "max": { "value": "16px", "type": "dimension" }
      }
    }
  }
}

Step 2: Calculate Fluid Values

Use this formula to calculate the viewport-relative value:

preferredSize = minSize + (maxSize - minSize) * ((100vw - minViewport) / (maxViewport - minViewport))

For h1 (32px → 48px, scaling between 640px and 1536px):

preferredSize = 32px + (48 - 32) * ((100vw - 640px) / (1536 - 640))
              = 32px + 16 * ((100vw - 640px) / 896)
              = 32px + 1.786vw - 11.43px
              = 20.57px + 1.786vw

Simplified:

font-size: clamp(32px, 1.786vw + 20.57px, 48px);

Step 3: Store as Token

FramingUI supports composite tokens:

{
  "typography": {
    "heading": {
      "h1": {
        "fontSize": {
          "value": "clamp(32px, 1.786vw + 20.57px, 48px)",
          "type": "dimension",
          "description": "Fluid h1 size (32px-48px, scales 640px-1536px)"
        }
      }
    }
  }
}

Step 4: Generate CSS Variables

npx framingui build

Output:

:root {
  --typography-heading-h1-font-size: clamp(32px, 1.786vw + 20.57px, 48px);
}

h1 {
  font-size: var(--typography-heading-h1-font-size);
}

Now every <h1> scales fluidly. No breakpoints needed.

Practical Fluid Type Scale

Here's a complete responsive type scale:

{
  "typography": {
    "heading": {
      "h1": {
        "fontSize": { "value": "clamp(2rem, 1.786vw + 1.286rem, 3rem)" }
      },
      "h2": {
        "fontSize": { "value": "clamp(1.75rem, 1.339vw + 1.161rem, 2.5rem)" }
      },
      "h3": {
        "fontSize": { "value": "clamp(1.5rem, 0.893vw + 1.107rem, 2rem)" }
      },
      "h4": {
        "fontSize": { "value": "clamp(1.25rem, 0.446vw + 1.054rem, 1.5rem)" }
      }
    },
    "body": {
      "lg": {
        "fontSize": { "value": "clamp(1rem, 0.223vw + 0.929rem, 1.125rem)" }
      },
      "base": {
        "fontSize": { "value": "clamp(0.875rem, 0.223vw + 0.804rem, 1rem)" }
      },
      "sm": {
        "fontSize": { "value": "clamp(0.75rem, 0.112vw + 0.721rem, 0.875rem)" }
      }
    }
  }
}

Generated CSS:

:root {
  --font-size-h1: clamp(2rem, 1.786vw + 1.286rem, 3rem);
  --font-size-h2: clamp(1.75rem, 1.339vw + 1.161rem, 2.5rem);
  --font-size-h3: clamp(1.5rem, 0.893vw + 1.107rem, 2rem);
  --font-size-body-lg: clamp(1rem, 0.223vw + 0.929rem, 1.125rem);
  --font-size-body-base: clamp(0.875rem, 0.223vw + 0.804rem, 1rem);
}

h1 { font-size: var(--font-size-h1); }
h2 { font-size: var(--font-size-h2); }
h3 { font-size: var(--font-size-h3); }

Result: Perfectly scaled typography across all devices with zero media queries.

Advanced: Fluid Line Height & Letter Spacing

Font size isn't the only thing that should scale. Line height and letter spacing should adapt too.

Fluid Line Height

Smaller text needs more relative line-height for readability:

{
  "typography": {
    "body": {
      "base": {
        "fontSize": { "value": "clamp(0.875rem, 0.223vw + 0.804rem, 1rem)" },
        "lineHeight": {
          "value": "clamp(1.4, 0.5vw + 1.3, 1.6)",
          "type": "number",
          "description": "Scales from 1.4 (mobile) to 1.6 (desktop)"
        }
      }
    }
  }
}

Result:

  • Mobile (small text): 1.4 line-height (tighter, more compact)
  • Desktop (larger text): 1.6 line-height (more breathing room)

Fluid Letter Spacing

Larger headings need tighter tracking:

{
  "typography": {
    "heading": {
      "h1": {
        "fontSize": { "value": "clamp(2rem, 1.786vw + 1.286rem, 3rem)" },
        "letterSpacing": {
          "value": "clamp(-0.02em, -0.01vw + -0.015em, -0.01em)",
          "type": "dimension",
          "description": "Tighter tracking on larger screens"
        }
      }
    }
  }
}

This creates a more refined, editorial feel as text scales up.

Using Fluid Typography in Components

React Component Example

export function Hero({ title, subtitle }) {
  return (
    <section className="py-16 px-4">
      <h1 className="font-bold text-heading-h1 leading-tight tracking-tight">
        {title}
      </h1>
      <p className="mt-4 text-body-lg text-text-secondary">
        {subtitle}
      </p>
    </section>
  );
}

Tailwind config (generated from tokens):

module.exports = {
  theme: {
    fontSize: {
      'heading-h1': 'clamp(2rem, 1.786vw + 1.286rem, 3rem)',
      'body-lg': 'clamp(1rem, 0.223vw + 0.929rem, 1.125rem)',
    }
  }
}

Result: Heading and subtitle scale smoothly across all screen sizes. No breakpoints in your component code.

Common Pitfalls

Pitfall 1: Overly Aggressive Scaling

/* ❌ Bad: scales too fast */
font-size: clamp(16px, 10vw, 64px);

On a 1200px viewport: 10vw = 120px → clamped to 64px (max).

But at 700px: 10vw = 70px → still 64px (max).

Problem: Font stays at max size for a wide range, then suddenly drops. Not smooth.

Solution: Use smaller viewport units (1-3vw):

/* ✅ Good: gradual scaling */
font-size: clamp(16px, 2vw + 12px, 64px);

Pitfall 2: Too Many Fluid Scales

You don't need clamp() for everything.

Use fluid scaling for:

  • ✅ Headings (h1-h4)
  • ✅ Hero section text
  • ✅ Display text (large, prominent text)

Keep fixed for:

  • ✅ Body text (reading comfort matters more than scaling)
  • ✅ UI labels (buttons, badges, tabs)
  • ✅ Small text (captions, footnotes)

Example:

{
  "typography": {
    "heading": {
      "h1": { "fontSize": "clamp(2rem, 2vw + 1rem, 3rem)" }  // Fluid
    },
    "body": {
      "base": { "fontSize": "1rem" }  // Fixed
    },
    "label": {
      "base": { "fontSize": "0.875rem" }  // Fixed
    }
  }
}

Pitfall 3: Forgetting Accessibility

Users can change their browser's default font size (usually 16px). Fluid typography should respect this.

Bad (px-based):

font-size: clamp(32px, 4vw, 48px);

If user sets browser font size to 20px, your heading stays 32-48px. Doesn't scale with user preference.

Good (rem-based):

font-size: clamp(2rem, 4vw, 3rem);

If user sets browser font size to 20px:

  • 2rem = 40px
  • 3rem = 60px

Your heading scales proportionally. Accessible by default.

Always use rem for min/max values in clamp():

/* ✅ Good */
font-size: clamp(1rem, 2vw + 0.5rem, 2rem);

/* ❌ Bad */
font-size: clamp(16px, 2vw + 8px, 32px);

Calculating Fluid Typography

The Manual Way

Formula:

clamp(MIN, (MIN + (MAX - MIN) * (100vw - MIN_VP) / (MAX_VP - MIN_VP)), MAX)

Example: Scale from 32px (640px viewport) to 48px (1536px viewport):

Slope = (48 - 32) / (1536 - 640) = 16 / 896 = 0.01786
Y-intercept = 32 - (0.01786 * 640) = 20.57

Result: clamp(32px, 1.786vw + 20.57px, 48px)

Problem: This math is tedious and error-prone.

The Easy Way: Use a Tool

Online calculators:

Input: Min/max sizes, min/max viewports
Output: clamp() formula

FramingUI (coming soon):

npx framingui generate-fluid-scale \
  --min-size 32px \
  --max-size 48px \
  --min-viewport 640px \
  --max-viewport 1536px

Output:

{
  "typography": {
    "heading": {
      "h1": {
        "fontSize": {
          "value": "clamp(2rem, 1.786vw + 1.286rem, 3rem)",
          "type": "dimension"
        }
      }
    }
  }
}

Design Token Structure for Typography

Basic Typography Tokens

{
  "typography": {
    "fontFamily": {
      "base": { "value": "Inter, sans-serif", "type": "fontFamily" },
      "heading": { "value": "Cal Sans, Inter, sans-serif", "type": "fontFamily" },
      "mono": { "value": "Fira Code, monospace", "type": "fontFamily" }
    },
    "fontSize": {
      "h1": { "value": "clamp(2rem, 2vw + 1rem, 3rem)", "type": "dimension" },
      "h2": { "value": "clamp(1.5rem, 1.5vw + 0.75rem, 2.25rem)", "type": "dimension" },
      "body": { "value": "1rem", "type": "dimension" }
    },
    "fontWeight": {
      "normal": { "value": "400", "type": "fontWeight" },
      "medium": { "value": "500", "type": "fontWeight" },
      "semibold": { "value": "600", "type": "fontWeight" },
      "bold": { "value": "700", "type": "fontWeight" }
    },
    "lineHeight": {
      "tight": { "value": "1.25", "type": "number" },
      "normal": { "value": "1.5", "type": "number" },
      "relaxed": { "value": "1.75", "type": "number" }
    },
    "letterSpacing": {
      "tight": { "value": "-0.02em", "type": "dimension" },
      "normal": { "value": "0", "type": "dimension" },
      "wide": { "value": "0.05em", "type": "dimension" }
    }
  }
}

Composite Text Styles

Group font properties together:

{
  "typography": {
    "style": {
      "heading-1": {
        "fontFamily": { "value": "{typography.fontFamily.heading}" },
        "fontSize": { "value": "{typography.fontSize.h1}" },
        "fontWeight": { "value": "{typography.fontWeight.bold}" },
        "lineHeight": { "value": "{typography.lineHeight.tight}" },
        "letterSpacing": { "value": "{typography.letterSpacing.tight}" }
      },
      "body": {
        "fontFamily": { "value": "{typography.fontFamily.base}" },
        "fontSize": { "value": "{typography.fontSize.body}" },
        "fontWeight": { "value": "{typography.fontWeight.normal}" },
        "lineHeight": { "value": "{typography.lineHeight.normal}" },
        "letterSpacing": { "value": "{typography.letterSpacing.normal}" }
      }
    }
  }
}

Generated CSS

FramingUI transforms these into CSS variables + utility classes:

:root {
  --font-family-heading: Cal Sans, Inter, sans-serif;
  --font-size-h1: clamp(2rem, 2vw + 1rem, 3rem);
  --font-weight-bold: 700;
  --line-height-tight: 1.25;
  --letter-spacing-tight: -0.02em;
}

.text-heading-1 {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-h1);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  letter-spacing: var(--letter-spacing-tight);
}

Usage:

<h1 className="text-heading-1">Welcome to FramingUI</h1>

One class. Fully responsive. Scales smoothly.

Integrating with Tailwind

Tailwind doesn't natively support clamp() in fontSize, but you can extend it:

// tailwind.config.js
module.exports = {
  theme: {
    fontSize: {
      'heading-1': 'clamp(2rem, 2vw + 1rem, 3rem)',
      'heading-2': 'clamp(1.5rem, 1.5vw + 0.75rem, 2.25rem)',
      'body': '1rem',
      'body-lg': 'clamp(1rem, 0.5vw + 0.875rem, 1.125rem)',
    },
    extend: {
      // Optionally add line-height and letter-spacing per text style
      lineHeight: {
        'tight': '1.25',
        'normal': '1.5',
      },
      letterSpacing: {
        'tight': '-0.02em',
        'normal': '0',
      }
    }
  }
}

Usage:

<h1 className="text-heading-1 leading-tight tracking-tight">
  Welcome
</h1>

AI Code Generation with Fluid Typography

Without Typography Tokens

Prompt: "Create a hero section with a heading and subtitle"

AI output:

<section className="py-20 text-center">
  <h1 className="text-5xl font-bold">Welcome to Our Product</h1>
  <p className="text-xl text-gray-600 mt-4">Build faster with AI</p>
</section>

Problems:

  • text-5xl is Tailwind's default (3rem fixed) — not responsive
  • text-xl (1.25rem fixed) — doesn't scale
  • Doesn't match your design system's fluid scale

With Typography Tokens

AI reads your tokens via MCP:

{
  "typography": {
    "style": {
      "hero-heading": {
        "fontSize": "clamp(2rem, 2vw + 1rem, 3rem)",
        "description": "Large hero heading with fluid scaling"
      },
      "hero-subtitle": {
        "fontSize": "clamp(1rem, 0.5vw + 0.875rem, 1.25rem)",
        "description": "Hero subtitle text"
      }
    }
  }
}

AI output:

<section className="py-20 text-center">
  <h1 className="text-hero-heading font-bold">Welcome to Our Product</h1>
  <p className="text-hero-subtitle text-text-secondary mt-4">
    Build faster with AI
  </p>
</section>

Result: Perfect responsive typography, matching your design system, generated automatically.

Viewport-Based vs. Container-Based

Viewport-Based (Standard)

font-size: clamp(2rem, 2vw, 3rem);

Scales based on viewport width (entire browser window).

Best for:

  • Page-level headings
  • Hero sections
  • Full-width content

Container-Based (Modern)

font-size: clamp(2rem, 5cqi, 3rem);

Scales based on container width (parent element), using Container Queries.

Best for:

  • Cards inside grids (card width varies)
  • Sidebar content (scales with sidebar width)
  • Reusable components that appear in different layouts

Browser support: Chrome 105+, Firefox 110+, Safari 16+ (2023+)

FramingUI support (coming soon):

{
  "typography": {
    "card": {
      "heading": {
        "fontSize": {
          "value": "clamp(1.25rem, 5cqi, 1.75rem)",
          "type": "dimension",
          "description": "Scales with card width"
        }
      }
    }
  }
}

Debugging Fluid Typography

Issue: Text Too Large on Mobile

Symptom: Heading is 48px on mobile, should be 32px.

Cause: Min value in clamp() is too high, or viewport unit is too large.

Fix:

/* ❌ Before */
font-size: clamp(48px, 4vw, 64px); /* Min is 48px */

/* ✅ After */
font-size: clamp(32px, 4vw, 64px); /* Min reduced to 32px */

Issue: Text Doesn't Scale

Symptom: Heading stays the same size on all screens.

Cause: Viewport unit is too small, or min/max are identical.

Fix:

/* ❌ Before */
font-size: clamp(32px, 0.1vw + 31.5px, 32.5px); /* Barely scales */

/* ✅ After */
font-size: clamp(32px, 2vw + 16px, 48px); /* Clear scaling range */

Issue: Text Scales Too Fast

Symptom: On a 1920px screen, heading is gigantic (80px+).

Cause: Max value is too high, or viewport unit is too large.

Fix:

/* ❌ Before */
font-size: clamp(32px, 5vw, 96px); /* Max too high */

/* ✅ After */
font-size: clamp(32px, 2vw + 16px, 56px); /* Max capped */

Debugging Tool

Use browser DevTools to inspect computed values:

// In browser console
const h1 = document.querySelector('h1');
console.log(getComputedStyle(h1).fontSize);

Resize window → check if font size changes smoothly.

Real-World Example: Building a Landing Page

Traditional Approach (Breakpoints)

<section className="py-12 md:py-16 lg:py-20">
  <h1 className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-bold">
    Build Faster with AI
  </h1>
  <p className="text-base md:text-lg lg:text-xl text-gray-600 mt-4">
    Design tokens that AI actually understands
  </p>
  <button className="mt-8 text-sm md:text-base lg:text-lg px-6 py-3">
    Get Started
  </button>
</section>

Problems:

  • 12+ responsive class names for one section
  • Typography "jumps" at breakpoints
  • Hard to maintain (every text element needs 4 size variants)

Fluid Approach (Tokens + clamp)

Define tokens:

{
  "typography": {
    "hero": {
      "heading": {
        "fontSize": { "value": "clamp(2rem, 3vw + 1rem, 4rem)" }
      },
      "subtitle": {
        "fontSize": { "value": "clamp(1rem, 1vw + 0.75rem, 1.5rem)" }
      }
    }
  },
  "spacing": {
    "section": {
      "padding-y": { "value": "clamp(3rem, 5vw, 6rem)" }
    }
  }
}

Component:

<section className="py-section">
  <h1 className="text-hero-heading font-bold">
    Build Faster with AI
  </h1>
  <p className="text-hero-subtitle text-text-secondary mt-4">
    Design tokens that AI actually understands
  </p>
  <button className="mt-8 text-base px-6 py-3">
    Get Started
  </button>
</section>

Result:

  • 7 fewer responsive classes
  • Smooth scaling (no jumps)
  • Easier to maintain (change tokens, not components)

FramingUI's Fluid Typography Workflow

Step 1: Define Fluid Scale

npx framingui init --fluid-typography

This generates a starter fluid type scale:

{
  "typography": {
    "fontSize": {
      "h1": { "value": "clamp(2rem, 2vw + 1rem, 3rem)" },
      "h2": { "value": "clamp(1.75rem, 1.5vw + 1rem, 2.5rem)" },
      "h3": { "value": "clamp(1.5rem, 1vw + 1rem, 2rem)" },
      "body-lg": { "value": "clamp(1rem, 0.5vw + 0.875rem, 1.125rem)" },
      "body": { "value": "1rem" }
    }
  }
}

Step 2: Build CSS + Tailwind

npx framingui build

Output:

/* CSS variables */
:root {
  --font-size-h1: clamp(2rem, 2vw + 1rem, 3rem);
  --font-size-h2: clamp(1.75rem, 1.5vw + 1rem, 2.5rem);
  --font-size-body-lg: clamp(1rem, 0.5vw + 0.875rem, 1.125rem);
}

/* Utility classes */
.text-h1 { font-size: var(--font-size-h1); }
.text-h2 { font-size: var(--font-size-h2); }
.text-body-lg { font-size: var(--font-size-body-lg); }
// Tailwind config
module.exports = {
  theme: {
    fontSize: {
      'h1': 'var(--font-size-h1)',
      'h2': 'var(--font-size-h2)',
      'body-lg': 'var(--font-size-body-lg)',
    }
  }
}

Step 3: Use in Components

export function Hero({ title, subtitle }) {
  return (
    <section className="py-20">
      <h1 className="text-h1 font-bold leading-tight">
        {title}
      </h1>
      <p className="text-body-lg text-text-secondary mt-4">
        {subtitle}
      </p>
    </section>
  );
}

Step 4: AI Generates Responsive Typography Automatically

When AI reads your design tokens via MCP, it sees:

typography.fontSize.h1: "clamp(2rem, 2vw + 1rem, 3rem)"
typography.fontSize.body-lg: "clamp(1rem, 0.5vw + 0.875rem, 1.125rem)"

AI understands: "These are responsive sizes—I should use them for headings and body text."

AI output:

<h1 className="text-h1 font-bold">AI-Generated Heading</h1>
<p className="text-body-lg">AI-generated content scales responsively</p>

No manual responsive classes. AI generates fluid typography from tokens.

Best Practices

1. Start with a Modular Scale

Use a mathematical ratio (1.25, 1.333, 1.5) to generate harmonious sizes:

Base: 16px (1rem)
Ratio: 1.25

Scale:
- 16px (1rem)
- 20px (1.25rem)
- 25px (1.563rem)
- 31px (1.953rem)
- 39px (2.441rem)
- 49px (3.052rem)

Then apply clamp() to each step:

{
  "fontSize": {
    "xs": "0.75rem",       // Fixed
    "sm": "0.875rem",      // Fixed
    "base": "1rem",        // Fixed
    "lg": "clamp(1rem, 0.5vw + 0.875rem, 1.25rem)",
    "xl": "clamp(1.25rem, 1vw + 0.938rem, 1.563rem)",
    "2xl": "clamp(1.563rem, 1.5vw + 1rem, 2rem)",
    "3xl": "clamp(2rem, 2vw + 1rem, 2.5rem)"
  }
}

2. Document Intended Use

Add descriptions so developers (and AI) know when to use each size:

{
  "typography": {
    "fontSize": {
      "h1": {
        "value": "clamp(2rem, 2vw + 1rem, 3rem)",
        "description": "Large page headings, hero titles (mobile: 32px, desktop: 48px)"
      }
    }
  }
}

3. Test Across Real Devices

Fluid typography looks different on actual phones vs. browser DevTools.

Test on:

  • iPhone SE (375px) — smallest common phone
  • iPhone 14 Pro (430px) — average phone
  • iPad (768px) — tablet
  • MacBook (1440px) — laptop
  • Desktop (1920px+) — large screens

Make sure text is readable at each size.

4. Combine with Fluid Spacing

Typography looks best when spacing scales proportionally:

{
  "spacing": {
    "section": {
      "padding-y": { "value": "clamp(3rem, 5vw, 6rem)" }
    }
  },
  "typography": {
    "fontSize": {
      "h1": { "value": "clamp(2rem, 2vw + 1rem, 3rem)" }
    }
  }
}

Result: Heading and section padding scale together—maintains visual balance.

Tools for Generating Fluid Scales

1. Utopia

utopia.fyi/type/calculator

Input:

  • Min/max font size
  • Min/max viewport
  • Type scale ratio

Output: Full clamp() formulas + CSS

2. Fluid Type Scale Calculator

fluid-type-scale.com

Visual editor for fluid scales. Adjust sliders → see live preview.

3. FramingUI CLI (Coming Soon)

npx framingui generate-type-scale \
  --base-size 16px \
  --ratio 1.25 \
  --min-viewport 640px \
  --max-viewport 1536px \
  --fluid-levels h1,h2,h3

Generates JSON tokens with clamp() formulas.

Conclusion

Fluid typography with design tokens eliminates breakpoint hell:

Traditional approach:

  • ❌ 5+ media queries per text element
  • ❌ Typography "jumps" at breakpoints
  • ❌ Hundreds of responsive class names
  • ❌ Hard to maintain, easy to forget breakpoints

Token-based fluid approach:

  • ✅ One clamp() formula per text style
  • ✅ Smooth scaling across all devices
  • ✅ One class per element (.text-h1)
  • ✅ Centralized in design tokens
  • ✅ AI generates responsive typography automatically

Start simple:

  1. Identify your most-used text styles (h1, h2, body)
  2. Define min/max sizes for each
  3. Calculate clamp() formulas (use Utopia or FramingUI)
  4. Store as design tokens
  5. Generate CSS variables + Tailwind utilities
  6. Replace breakpoint-based typography with fluid classes

Your typography will scale smoothly, your code will be cleaner, and AI will generate perfectly responsive text on the first try.

Ready to eliminate media query hell? Try FramingUI.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts