CSS and Layout skill 4 of 5

Fluid Type SKILL.md: Fix clamp() Text That Fails Zoom

The Fluid Type and Spacing skill makes Claude, Cursor or another AI agent find text sized in vw and clamp() values that fail browser zoom, then rebuild them with a calculator that tests 500% zoom. A read-only checker flags nine problems, and the agent edits in small batches you approve.

fluid-type-spacing/SKILL.md, version 1.0.0, 3,170 words, plus scripts/fluid_check.py and scripts/fluid_calc.py.

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.

RequestUse instead
Component that changes with its container, cqi unitscontainer-queries.md
100vh, dvh, svh, mobile browser barsviewport-units.md
Naming and structuring tokens, @property, fallbackscss-custom-properties.md
Horizontal overflow, truncation, 1fr blowoutsflexbox-grid-layout.md
A rule that loses to another rulecascade-layers.md
Choosing typefaces, the base size or the ratioOut of scope. This skill adapts an existing scale
Full WCAG audit or conformance statementOut 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.

InputAsk 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?"

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as fluid-type-spacing/SKILL.md.

---
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.

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.

CodeLevelMeaningFix
vw-onlyerrorfont-size in viewport units alone never grows with zoom (WCAG failure F94)clamp() with a rem term (step 4)
zoom-failerrorA clamp() that grows less than 2.0x at 500% zoom, with the worst width shownLower the max or raise the min (step 4)
min-gt-maxerrorThe clamp() minimum is above the maximum, so the minimum wins and nothing movesSwap or correct the bounds
zoom-blockederroruser-scalable=no in the viewport meta tagRemove it
px-boundswarnclamp() bounds in px ignore the user's font-size settingConvert to rem
calc-unboundedwarncalc() with viewport units and no clamp() has no cap at wide screensWrap in clamp()
root-pxwarnhtml { font-size: 16px } overrides the user's default sizeUse % or remove it
line-height-fixedwarnA rem, px or vw line-height on fluid text does not follow the sizeUnitless value
zoom-limitedwarnmaximum-scale limits pinch zoomRemove 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

TextDecisionWhy
Body copy, form labels, table text, buttons, captionsFixed rem, or a few stepped sizesDense UI reads best at stable sizes. Keep body at 1rem or above
Headings, display text, hero textFluid clamp()Large sizes gain the most from scaling
Text inside a reusable componentFluid on cqi only after container-queries.md has declared a containerWith no container, cqi falls back to small viewport units (MDN)
Section spacing, gutters, page paddingFluid, step 5No zoom limit applies to spacing
Spacing inside buttons, inputs, table cellsFixedFluid 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

ItemDefaultRule
Start width320pxThe smallest width the user supports
Stop width1240pxThe layout's maximum content width. After this width the size must stop growing
Size ratioThe scale's ownDo 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.

ResultAction
PassUse the string as printed
FAILThe 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 minAlways passes (Smashing Magazine, 7 November 2023; 3,000 random ranges agreed)
Max is above 2.5 times the minUsually fails. A very wide range can still pass, so trust the script
Whole scale neededreferences/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

RuleReason
Put every size in a custom property on :root, one per stepOne place to edit. Naming and structure are in css-custom-properties.md
Use the printed string verbatimHand-rounded values drift from the tested ones
line-height is unitlessA 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 blocksThe 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-scaleThey 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

MistakeHow to spot itDo instead
Sizing text in vw alone, or calc() with vw and no capvw-only, calc-unbounded. Text stays the same size under zoomclamp() with a rem term, tested by the calculator
A heading maximum far above the minimumzoom-fail, or the calculator says FAILCap the max at the printed value, or raise the min
Bounds in px, or html { font-size: 16px }px-bounds, root-pxrem bounds. Leave the root size to the user
Fixed line-height on fluid textline-height-fixed. Lines touch or gap at some widthsUnitless line-height
Checking only the narrowest and widest widthsHeadings wrap badly or overflow at a mid widthView 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.

ItemTokensStatusRead condition
A. Description (always loaded)117EstimateEvery session with the skill installed
B. SKILL.md body4,481EstimateWhen the skill fires
C. references/scale-patterns.md514EstimateOnly when building a whole scale
C. references/zoom-test.md395EstimateOnly when a browser measurement is wanted
C. references/appendix.md634EstimateOnly to check a claim
Script output412EstimateEvery run. Checker output is capped with head -40
D. Typical run (A + B + script output)5,010EstimateCommon path: fix flagged lines, no references
D. Worst case (A + B + all references + 40 lines of output)7,411EstimateWhole scale, browser test, many findings
E. Cheaper path4,980EstimateRun 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.

What this skill does

Fluid type looks fine on a laptop and fails quietly under zoom. Browsers do not scale viewport units when a user zooms the page, so a heading set in vw stays the same size at every zoom level. A clamp() with a high maximum does the same at wide screens. In a test published by Smashing Magazine, clamp(16px, 5.33px + 3.33vw, 48px) grew only 1.67 times at 500% zoom, which fails WCAG 1.4.4.

This skill gives the agent a fixed order. A read-only checker reads the stylesheets and reports nine kinds of problem. The agent decides which text should be fluid, agrees a viewport range with you and builds each clamp() with a calculator that returns pass or FAIL at 500% zoom and, on failure, the largest maximum that passes. It writes the values, runs both scripts again and waits for your approval before each batch.

Who it is for

Use it if you ship a type scale built on clamp(), inherit a stylesheet full of vw font sizes, review a pull request against WCAG 1.4.4, or want fluid spacing without guessing. It suits developers who use Claude Code, Cursor or similar agents.

It is not for container query setup, which the Container Queries skill covers, or viewport heights, which belong to Viewport Units. It does not choose typefaces, base sizes or ratios. It is also not an accessibility audit: a pass is evidence for one criterion, and a person should review the result before anyone claims conformance.

What is inside

The file is 3,170 words (version 1.0.0) with two scripts and three reference files. It holds:

  • Eight guardrails, including no live-site measuring, no secrets in reports, a human accessibility review before any conformance claim, and a refusal to hide a failing result by widening the viewport range.
  • Six inputs the agent must have first, with the exact question for each.
  • A workflow of steps 0 to 8, a table with one fix per checker code, and a table that decides which text is fluid, which is stepped and which stays fixed.
  • Two standard-library scripts: scripts/fluid_check.py with nine problem codes, and scripts/fluid_calc.py, which builds one clamp() or a whole scale and tests it against 500% zoom.
  • A worked example with real output (6 errors and 5 warnings before, 0 after), measured in Chromium 153.
  • A report template, a starting prompt, five failure modes and an eight-item quality bar.
  • Three reference files: scale and token recipes, a browser zoom script, and an appendix with 11 measurements and the source claims.

The token costs below are estimates from the skill’s own count: words times 1.35 for prose and characters divided by 3.5 for code and tables. They are not from a model tokeniser.

ItemTokensStatus
Description (always loaded)117Estimate
SKILL.md body (when the skill fires)4,481Estimate
references/scale-patterns.md, read only when building a whole scale514Estimate
references/zoom-test.md, read only when a browser measurement is wanted395Estimate
references/appendix.md, read only to check a claim634Estimate
Typical run (description, body, script output)5,010Estimate
Cheaper path (checker, calculator on flagged values only, no references)4,980Estimate
Worst case (all references, 40 lines of checker output)7,411Estimate

This skill costs more to load than the three rivals I could size, which are roughly 1,500 to 2,600 tokens for SKILL.md alone. What it adds is a zoom test that returns pass or fail, and a checker that reads the stylesheet so the agent does not. On a stylesheet under about 100 lines, the skill says to read the file and use the calculator only.

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as fluid-type-spacing/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the scripts beside your code. Then send the starting prompt from the Templates section.

Before you install. Read both scripts first. They read CSS and HTML files and write nothing. The skill tells the agent to edit only on a branch in batches you approve. Install skills only from sources you trust.

A worked example

An invented stylesheet has eight rules, and the HTML page blocks zoom. The checker reports 6 errors and 5 warnings, including a heading in 5vw, an h2 clamp that grows 1.67 times at 500% zoom, a clamp with its minimum above its maximum, and user-scalable=no.

The design asks for an h2 from 1rem to 3rem between 320 and 1240px. The calculator prints FAIL at 1.67 times and names 2.5rem as the largest passing maximum. The user agrees, and the agent writes clamp(1rem, 0.4783rem + 2.6087vw, 2.5rem), which passes at exactly 2.00 times. One batch fixes the rest. The checker then reports no errors or warnings, and in Chromium 153 the new h2 grew 2.0 times at 500% zoom on a 1280px screen.

Common mistakes

The skill is built around five ways this goes wrong:

  • Sizing text in vw alone. It never grows with zoom. The skill uses clamp() with a rem term and tests it.
  • A heading maximum far above the minimum. Growth under zoom falls below 2 times. The skill caps the maximum at the value the calculator prints.
  • Bounds in px and a root size in px. Users who raise their default size lose the change. The skill uses rem and leaves the root alone.
  • Fixed line-height on fluid text. Lines touch or gap at some widths. The skill uses a unitless value.
  • Checking only the narrowest and widest widths. Headings wrap badly in the middle. The skill requires one mid-width view.

Container Queries covers cqi sizing inside components, and Flexbox and Grid Layout covers overflow at mid widths. Viewport Units covers dvh and svh, and CSS Custom Properties covers how to structure the size tokens. Browse the CSS and Layout set, the Frontend Development category or the Skills-md library.

Frequently asked questions

Browsers do not scale viewport units when you zoom the page, so the vw part of a clamp() shrinks as zoom rises while the size is capped by its maximum. If the maximum is far above the minimum, text grows less than the required 2 times at 500% zoom. In Chromium 153 a 1rem to 3rem clamp grew 1.67 times. The skill tests this for you.

If the maximum font size is at most 2.5 times the minimum, a clamp() always passes the zoom test on modern browsers. Smashing Magazine published the rule in November 2023. The skill checked it against 3,000 random ranges with no failures. Above 2.5 times a value usually fails, but a very wide viewport range can still pass, so the calculator decides.

Viewport units stay tied to the window, and zooming the page shrinks the CSS viewport. A heading set in 5vw measured the same size at 100% and 500% zoom in Chromium 153, a growth of 1.0. WCAG lists this as failure F94. Replace it with a clamp() that has a rem term and rem bounds, and run the calculator.

WCAG 1.4.4 does not list px as a failure, but px bounds ignore a user who raised their default font size. A rem term followed a 32px root size in Chromium 153, giving 41.6px against 33.6px at 16px. The skill flags px bounds as a warning and asks you to convert them to rem. It also flags a px root size.

Work out the slope as the size change divided by the viewport change, then the intercept from the minimum. For 1rem to 2rem across 320 and 1440px that gives clamp(1rem, 0.7143rem + 1.4286vw, 2rem). The skill runs this for you with fluid_calc.py, adds the zoom test, and prints the largest maximum that passes if yours fails.

Yes. A fixed rem line-height does not follow a fluid font size. In Chromium 153, 64px text with a 3rem line-height had a 48px line box, while a unitless 1.2 gave 76.8px. Lines then touch at some widths and gap at others. The skill flags rem, px and vw line-heights on fluid text and asks for a unitless value.

The description is about 117 tokens and loads in every session. The SKILL.md body is about 4,481 tokens when the skill fires. A typical run is about 5,010 tokens with checker output, the cheapest path is about 4,980 and the worst case is about 7,411. All figures are estimates, not from a model tokeniser.

Changelog

  • 1.0.0, 27 October 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.