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.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 27 October 2026. Updated 27 October 2026. Free to use under CC BY 4.0.
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
- 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.
- Never edit vendor files. Do not change
node_modules, vendor folders or CDN copies. Override them from your own CSS. - 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. - Batches stay small. At most 1 scale or 20 declarations per batch (defaults), each shown as a diff.
- Keep what works. Leave a fixed
1rembody size alone. Do not make dense UI text fluid (step 2). - 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.
- 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.
- Refuses to: set text in
vwalone, adduser-scalable=noormaximum-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?" |
Raw SKILL.md file
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 -40Read 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 remFor 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 0Ignore 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
- Run
fluid_check.pyagain. Errors must be 0. Each remaining warning is listed with a reason. - Run
fluid_calc.pyon every changed value. Every line must say pass. - 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.mdif it overflows. - If a browser measurement is wanted, use
references/zoom-test.md. - Run the existing test suite if there is one.
- 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 warningsThe 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.5remThe 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.
- Checker errors are 0, and each warning is listed with a reason.
- Every changed
clamp()was printed byfluid_calc.pyand reported pass. - No
font-sizeis in viewport units without a rem term and bounds. - No
user-scalable=noormaximum-scaleremains. - Body text is 1rem or larger at every width.
- The page was viewed at 320px, one mid width and the stop width.
- Every changed line appears in the report with file, line, issue and fix.
- 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
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.pywith nine problem codes, andscripts/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.
| Item | Tokens | Status |
|---|---|---|
| Description (always loaded) | 117 | Estimate |
| SKILL.md body (when the skill fires) | 4,481 | Estimate |
references/scale-patterns.md, read only when building a whole scale | 514 | Estimate |
references/zoom-test.md, read only when a browser measurement is wanted | 395 | Estimate |
references/appendix.md, read only to check a claim | 634 | Estimate |
| Typical run (description, body, script output) | 5,010 | Estimate |
| Cheaper path (checker, calculator on flagged values only, no references) | 4,980 | Estimate |
| Worst case (all references, 40 lines of checker output) | 7,411 | Estimate |
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.
Related skills
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
Why does clamp() fail WCAG 1.4.4?
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.
What is the 2.5 times rule for fluid font sizes?
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.
Why does my vw font size not grow when I zoom?
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.
Is it bad to use px in clamp()?
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.
How do I calculate a clamp() for fluid typography?
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.
Should line-height be unitless with fluid type?
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.
How many tokens does the Fluid Type and Spacing skill use?
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.
