---
name: fluid-type-spacing
description: "Audits and fixes fluid type and spacing in CSS: font-size set in vw only, clamp() values that fail 500% page zoom, px bounds, a max more than 2.5 times the min, fixed line-height on fluid text, and zoom-blocking viewport meta tags. Runs a read-only checker, builds clamp() strings with a calculator that tests zoom, and generates scales. Use for fluid typography, clamp(), responsive font sizes, type scales, WCAG 1.4.4 resize text. Do NOT use for container query setup, viewport height units, custom property architecture or layout overflow."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/fluid-type-spacing/
  category: frontend-development
  subcategory: css-layout
  version: "1.0.0"
  created: "2026-10-27"
  updated: "2026-10-27"
---

# Fluid Type and Spacing: Scale Text Between Viewports Without Failing Zoom

An agent works on fluid type in this order: run a read-only checker over the stylesheets, decide which text should be fluid at all, pick a viewport range, build each `clamp()` with a calculator that tests it against 500% page zoom, write the values, then re-run both scripts. Spacing uses the same calculator without the zoom test.

It works on plain CSS, bundler output and `<style>` blocks. Browser measurements and source claims sit in `references/appendix.md`, whole scales in `references/scale-patterns.md`, a browser zoom script in `references/zoom-test.md`.

## When to use this skill, and when not to

Use it when text is sized in `vw`, when a `clamp()` heading does not grow under browser zoom, when someone asks for a type scale or fluid spacing, or when a review cites WCAG 1.4.4 on resizing text.

| Request | Use instead |
|---|---|
| Component that changes with its container, `cqi` units | `container-queries.md` |
| `100vh`, `dvh`, `svh`, mobile browser bars | `viewport-units.md` |
| Naming and structuring tokens, `@property`, fallbacks | `css-custom-properties.md` |
| Horizontal overflow, truncation, `1fr` blowouts | `flexbox-grid-layout.md` |
| A rule that loses to another rule | `cascade-layers.md` |
| Choosing typefaces, the base size or the ratio | Out of scope. This skill adapts an existing scale |
| Full WCAG audit or conformance statement | Out of scope. This skill tests one criterion |

## Guardrails

1. **Propose, then change.** Report and plan first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Never edit vendor files.** Do not change `node_modules`, vendor folders or CDN copies. Override them from your own CSS.
3. **No page text, no live sites, no secrets.** The scripts read CSS and viewport meta tags only. Do not fetch or measure pages you do not own. Stylesheets can carry API keys or tokens in `url(...)` values and comments: do not paste them into reports or prompts.
4. **Batches stay small.** At most 1 scale or 20 declarations per batch (defaults), each shown as a diff.
5. **Keep what works.** Leave a fixed `1rem` body size alone. Do not make dense UI text fluid (step 2).
6. **Nothing is invented.** Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry **verify before relying on this**. Min and max sizes and the viewport range come from the design or the user, never from a guess.
7. **Not accessibility advice.** A passing zoom test is evidence for one criterion, not a conformance claim. Say so in the report, and ask for a human accessibility review before anyone states WCAG compliance.
8. **Refuses to:** set text in `vw` alone, add `user-scalable=no` or `maximum-scale`, hide a failing result by widening the viewport range, or call a design accessible on this test alone.

## Inputs the agent must have before starting

Ask for every missing item. Do not assume.

| Input | Ask exactly |
|---|---|
| Stylesheets | "List every stylesheet and the HTML that loads it, in load order." |
| Sizes | "What are the minimum and maximum size for each step, or which scale and ratio should I follow?" |
| Range | "At what viewport width should text start to grow, and at what width should it stop? The stop width is usually the layout's maximum width." |
| What is fluid | "Which text should be fluid: headings and hero text only, or more?" |
| Spacing | "Should section spacing and gutters be fluid too?" |
| Approval | "May I edit on a branch in batches, showing each diff first?" |

## Workflow

### Step 0: Confirm scope

List the files. Minified input is fine for the checker, but edits go in source files. If the project builds CSS from tokens, find the file that declares them.

### Step 1: Run the checker

```
python3 scripts/fluid_check.py page.css page.html | head -40
```

Read only its output. Do not read the script. It accepts CSS and HTML. The last line gives counts. It exits 1 on any error, and `--strict` also fails on warnings.

| Code | Level | Meaning | Fix |
|---|---|---|---|
| `vw-only` | error | `font-size` in viewport units alone never grows with zoom (WCAG failure F94) | `clamp()` with a rem term (step 4) |
| `zoom-fail` | error | A `clamp()` that grows less than 2.0x at 500% zoom, with the worst width shown | Lower the max or raise the min (step 4) |
| `min-gt-max` | error | The `clamp()` minimum is above the maximum, so the minimum wins and nothing moves | Swap or correct the bounds |
| `zoom-blocked` | error | `user-scalable=no` in the viewport meta tag | Remove it |
| `px-bounds` | warn | `clamp()` bounds in px ignore the user's font-size setting | Convert to rem |
| `calc-unbounded` | warn | `calc()` with viewport units and no `clamp()` has no cap at wide screens | Wrap in `clamp()` |
| `root-px` | warn | `html { font-size: 16px }` overrides the user's default size | Use `%` or remove it |
| `line-height-fixed` | warn | A rem, px or vw `line-height` on fluid text does not follow the size | Unitless value |
| `zoom-limited` | warn | `maximum-scale` limits pinch zoom | Remove it unless a test shows zoom still works |

Pass every file that declares or uses fluid sizes. If the output is empty apart from the count line, nothing is wrong that the checker can see, and you move to step 7. The checker skips `var()` values and nested selectors, so run it on the file that declares the tokens.

### Step 2: Decide what is fluid

| Text | Decision | Why |
|---|---|---|
| Body copy, form labels, table text, buttons, captions | Fixed rem, or a few stepped sizes | Dense UI reads best at stable sizes. Keep body at 1rem or above |
| Headings, display text, hero text | Fluid `clamp()` | Large sizes gain the most from scaling |
| Text inside a reusable component | Fluid on `cqi` only after `container-queries.md` has declared a container | With no container, `cqi` falls back to small viewport units (MDN) |
| Section spacing, gutters, page padding | Fluid, step 5 | No zoom limit applies to spacing |
| Spacing inside buttons, inputs, table cells | Fixed | Fluid values here make controls change size mid-gesture |

The first two rows follow the harness-engineering and jpoindexter skills (see Sources). They are defaults. If the design says otherwise, follow the design.

### Step 3: Choose the viewport range

| Item | Default | Rule |
|---|---|---|
| Start width | 320px | The smallest width the user supports |
| Stop width | 1240px | The layout's maximum content width. After this width the size must stop growing |
| Size ratio | The scale's own | Do not invent one. If none is given, ask |

The 320 and 1240 defaults come from the Utopia-style example in the harness-engineering skill. The Utopia article uses 320 and 1440. Both are examples, not standards.

Do not widen the stop width to make a failing size pass. It buys little: for a 1rem minimum the largest passing maximum was 2.5rem at stop widths up to 1600px and 3.2rem at 3840px (measured, appendix).

### Step 4: Calculate each value

```
python3 scripts/fluid_calc.py MIN MAX FROM TO --unit rem
```

For example `fluid_calc.py 1 2 320 1240 --unit rem`. It prints one `clamp()` string and one pass or FAIL line, and exits 1 on failure. Do not work out slope and intercept by hand.

| Result | Action |
|---|---|
| Pass | Use the string as printed |
| FAIL | The script prints the largest passing max. Use it, or raise the min, and confirm with the design owner. Re-run |
| Max is at most 2.5 times the min | Always passes (Smashing Magazine, 7 November 2023; 3,000 random ranges agreed) |
| Max is above 2.5 times the min | Usually fails. A very wide range can still pass, so trust the script |
| Whole scale needed | `references/scale-patterns.md`, one call generates every step |

The test models browser page zoom: at zoom Z the viewport shrinks to width / Z and text is Z times larger in device pixels. The script requires at least 2x growth at 500% zoom across 320 to 3840px. Chromium 153 matched the model to three decimals. Pinch zoom on a phone does not change viewport units and is outside this model.

### Step 5: Spacing

Use the same script with the zoom test turned off:

```
python3 scripts/fluid_calc.py 12 20 320 1240 --need 0
```

Ignore the zoom column. Keep spacing bounds in rem. Section gaps and gutters may grow with the viewport. Do not make a spacing value larger than the design's maximum just because the range allows it.

### Step 6: Write the CSS

| Rule | Reason |
|---|---|
| Put every size in a custom property on `:root`, one per step | One place to edit. Naming and structure are in `css-custom-properties.md` |
| Use the printed string verbatim | Hand-rounded values drift from the tested ones |
| `line-height` is unitless | A fixed rem line-height on 64px text gave a 48px line box (measured) |
| Pair a fluid size with a `max-width` in `ch` on text blocks | The line length must follow the size. Take the number from the design |
| Leave `html { font-size }` unset or in `%` | Users who raise their default size keep it. A rem term followed a 32px root (measured) |
| Do not add `text-size-adjust`, `user-scalable=no` or `maximum-scale` | They block text resizing |

### Step 7: Verify

1. Run `fluid_check.py` again. Errors must be 0. Each remaining warning is listed with a reason.
2. Run `fluid_calc.py` on every changed value. Every line must say pass.
3. View the page at 320px, a mid width such as 700px and the stop width. A heading that fits at both ends can still wrap badly in the middle. Use `flexbox-grid-layout.md` if it overflows.
4. If a browser measurement is wanted, use `references/zoom-test.md`.
5. Run the existing test suite if there is one.
6. List what you could not test: older browsers, pinch zoom, user stylesheets, text-only zoom.

### Step 8: Report

Use the report template. A short list of file, line, issue and fix.

## Worked example

An invented stylesheet with eight rules and an HTML page that blocks zoom. The agent runs the checker.

```
page.css:1  warn  root-px  html: font-size 16px on the root overrides the user's default size; use % or rem
page.css:2  warn  line-height-fixed  h1: line-height 6vw does not follow the fluid font-size; use a unitless value
page.css:2  error  vw-only  h1: font-size 5vw is viewport units only and never grows with zoom (WCAG F94). Use clamp() with a rem term
page.css:3  warn  px-bounds  h2: bound 16px is in px and ignores the user's font-size setting; use rem
page.css:3  error  zoom-fail  h2: max/min 3.00x; text grows only 1.67x at 500% zoom (worst at 1290px). Cap the max at 2.5x the min
page.css:5  warn  calc-unbounded  h4: font-size calc(1rem + 2vw) has no clamp() bounds; add a rem minimum and maximum
page.css:6  error  min-gt-max  .big: clamp minimum 3rem is above maximum 2rem; the minimum wins, so the size never moves
page.css:7  warn  line-height-fixed  .hero: line-height 3rem does not follow the fluid font-size; use a unitless value
page.css:7  error  zoom-fail  .hero: max/min 3.20x; text grows only 1.80x at 500% zoom (worst at 2330px). Cap the max at 2.5x the min
page.html:2  error  zoom-blocked  meta viewport: user-scalable=no blocks pinch zoom; remove it
page.html:3  error  vw-only  p: font-size 4vw is viewport units only and never grows with zoom (WCAG F94). Use clamp() with a rem term
6 errors, 5 warnings
```

The `h3` and `.ok` rules are not listed because they pass. The design asks for `h2` at 1rem to 3rem between 320 and 1240px. The agent runs the calculator:

```
$ python3 scripts/fluid_calc.py 1 3 320 1240 --unit rem
clamp(1rem, 0.3043rem + 3.4783vw, 3rem)  max/min 3.00x  zoom 1.67x@1240px FAIL  largest passing max 2.5rem
```

The agent reports the failure, proposes 2.5rem and asks the user whether the design can take it. The user agrees, and the calculator gives `clamp(1rem, 0.4783rem + 2.6087vw, 2.5rem)`, which passes at exactly 2.00x. The `h1` and `.hero` design sizes (1.25rem to 3.1rem) pass at 2.02x with `clamp(1.25rem, 0.6065rem + 3.2174vw, 3.1rem)`. In one batch (`page.css`, `page.html`) the agent also sets `html` to 100%, gives `h4` a capped clamp, puts the `.big` bounds the right way round, switches `line-height` to 1.1, and removes `user-scalable=no`. The checker then reports 0 errors and 0 warnings. In Chromium 153 on a 1280px screen the old `h2` grew 1.67x at 500% zoom and the new one 2.0x, measured on 27 October 2026.

## Templates

### Report

```
Fluid type report: <site or page>, <date>
Range: <start>px to <stop>px. Scale: <name, ratio, or "from design">.
Checker before: N errors, N warnings. After: N errors, N warnings.
Changes: <file:line  issue  fix>   (one line each)
Calculator: <value  result>   (one line each)
Left as is: <warning  reason>
Not tested: <items>
Review needed: a human accessibility review before any conformance claim.
```

### Starting prompt

```
Use the fluid-type-spacing skill. Stylesheets: <list>. Fluid text: <headings only | list>.
Range: <start> to <stop>px. Sizes: <min and max per step, or the scale>.
Edit on a branch in batches of one scale or 20 declarations and show each diff first.
```

## Failure modes and anti-patterns

| Mistake | How to spot it | Do instead |
|---|---|---|
| Sizing text in `vw` alone, or `calc()` with `vw` and no cap | `vw-only`, `calc-unbounded`. Text stays the same size under zoom | `clamp()` with a rem term, tested by the calculator |
| A heading maximum far above the minimum | `zoom-fail`, or the calculator says FAIL | Cap the max at the printed value, or raise the min |
| Bounds in px, or `html { font-size: 16px }` | `px-bounds`, `root-px` | rem bounds. Leave the root size to the user |
| Fixed line-height on fluid text | `line-height-fixed`. Lines touch or gap at some widths | Unitless line-height |
| Checking only the narrowest and widest widths | Headings wrap badly or overflow at a mid width | View at least one mid width (step 7) |

## Quality bar

Run every item before handing over.

1. Checker errors are 0, and each warning is listed with a reason.
2. Every changed `clamp()` was printed by `fluid_calc.py` and reported pass.
3. No `font-size` is in viewport units without a rem term and bounds.
4. No `user-scalable=no` or `maximum-scale` remains.
5. Body text is 1rem or larger at every width.
6. The page was viewed at 320px, one mid width and the stop width.
7. Every changed line appears in the report with file, line, issue and fix.
8. The report lists what was not tested and asks for a human accessibility review.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from summaries of the pages I read, so they carry two layers of estimate.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 117 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 4,481 | Estimate | When the skill fires |
| C. `references/scale-patterns.md` | 514 | Estimate | Only when building a whole scale |
| C. `references/zoom-test.md` | 395 | Estimate | Only when a browser measurement is wanted |
| C. `references/appendix.md` | 634 | Estimate | Only to check a claim |
| Script output | 412 | Estimate | Every run. Checker output is capped with `head -40` |
| D. Typical run (A + B + script output) | 5,010 | Estimate | Common path: fix flagged lines, no references |
| D. Worst case (A + B + all references + 40 lines of output) | 7,411 | Estimate | Whole scale, browser test, many findings |
| E. Cheaper path | 4,980 | Estimate | Run the checker, run the calculator on flagged values only, skip the references |

Rival sizes, estimated: dylantarre responsive-typography about 1,300 to 1,500 words (roughly 1,755 to 2,025 tokens, no scripts); harness-engineering design-responsive-type about 1,900 words (roughly 2,565 tokens, no scripts); jpoindexter responsive-typography about 1,100 to 1,300 words (roughly 1,485 to 1,755 tokens, no scripts). This skill costs more to load than all three. What it adds is a zoom test that returns pass or fail instead of asking the agent to judge, and a checker that reads the stylesheet so the agent does not. On a 1,168-rule stylesheet the checker printed one line in 47ms. On a stylesheet under about 100 lines, read the file and use the calculator only.

## Handoffs

`container-queries.md` covers `cqi` sizing inside components. `viewport-units.md` covers `dvh` and `svh`. `css-custom-properties.md` covers token structure. `flexbox-grid-layout.md` covers overflow at mid widths.

## Appendix

Measurements and source claims are in `references/appendix.md`. Scale and token recipes are in `references/scale-patterns.md`. The browser script is in `references/zoom-test.md`.

## Sources

Last verified 27 October 2026 for every source.

- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Smashing Magazine, Addressing Accessibility Concerns With Using Fluid Type (7 November 2023): https://www.smashingmagazine.com/2023/11/addressing-accessibility-concerns-fluid-type/
- W3C, Understanding SC 1.4.4 Resize Text: https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html
- Utopia, Designing with fluid type scales, clamp: https://utopia.fyi/blog/clamp/
- MDN, CSS length units: https://developer.mozilla.org/en-US/docs/Web/CSS/length
- dylantarre, responsive-typography. Rival implementation: https://skills.cat/skills/dylantarre/design-system-skills/responsive-typography
- intense-visions, harness-engineering design-responsive-type. Rival implementation: https://openskillindex.com/skills/intense-visions-harness-engineering-design-responsive-type-6cf62e
- jpoindexter, responsive-typography. Rival implementation: https://www.skillsdirectory.com/skills/jpoindexter-responsive-typography

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/fluid-type-spacing/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
