Troubleshooting — by symptom
Start from what you are seeing, not from what you think is broken. Every entry names the cause and the fix; most of these were bugs we shipped ourselves first.
Authoring
The value never changes — "the library does nothing"
Almost always a container-bound value whose range is the viewport's. { container: true } changes what is measured, not the range it is measured over, so a card living between 240px and 820px inside a [320, 1440] project walked a fifth of its curve. As of 1.0.0-alpha.0 this is refused at construction:
r$: fluid() is bound to its container but never says how wide that container gets,
so it would interpolate over the viewport breakpoints instead.
Declare the range: { container: true, from: <narrowest px>, to: <widest px> }Fix: r$.fluid(15, 26, { container: true, from: 240, to: 820 }).
If the value is not container-bound and still looks frozen, check that the viewport is inside [from, to] — outside the domain a fluid value is clamped, which is correct and looks static.
An attribute toggles forever, the layout flickers
You are measuring the element you restyle. The predicate sets data-x, your CSS changes that element's size, the measurement flips back, forever. Two honest ways out:
- Collapse keeping layout:
visibility: hidden; height: 0; overflow: hidden— neverdisplay: noneon what a predicate measures. - Measure a probe: keep a hidden element with the natural dimensions, never restyled, and measure that. The table-to-cards demo on the site does exactly this.
Corollary that bites in components: styling the measured element's children counts too. Making pills flex: 1 1 100% when their row wraps means they will wrap forever and never come back — a latch, not an oscillation, and harder to spot. Style something the measurement does not depend on (:has() is useful here).
A predicate is never true
whenWrapsneeds at least two children; with one it is alwaysfalseby definition.whenTruncatedneeds an axis that is actually clipped —overflow: hiddenorclipplus content that exceeds it. Without the clipping there is nothing to detect.- Anything on a
display: noneelement measures a zero box and never fires. whenStuckneedsposition: stickyand atop/bottomoffset to be pinned against.
Styles vanish after a re-render
The construct was bound to elements that no longer exist. In a component, group the handles and dispose them with the component:
const s = r$.scope();
s.add(r$('.card', { padding: r$.fluid(12, 28) }));
onUnmount(() => s.dispose());For a selector that must keep matching elements as they come and go, use r$.observe(selector, map) — it injects the static half once and manages the JS half per element.
Server-rendered values are wrong until hydration
The CSS-first half is fully server-renderable — r$.static(selector, map) gives you the stylesheet. The JS half resolves at config.ssrWidth until the first client effect, so if a value matters above the fold, keep it linear so it compiles to clamp() and never needs JS.
The CLI
"no driver available"
npx @responsivejs/cli doctorOne line per check — Node version, Playwright and its Chromium, agent-browser on PATH — with the exact install command for anything missing, and which driver auto will pick. Exit 0 means at least one driver is usable.
It passed, but the count says 794/799
Those five are warnings, not failures. pass means zero errors; warnings are things worth a look that do not break the build (content that overflows inside a scroll container, for instance). Use --strict to fail on them too.
The contract fails after a design change you meant to make
That is the baseline doing its job: a curve moved. If the change is intended, re-pin it and commit the diff —
npx @responsivejs/cli record site.contract.json https://your-site.com— so the new curve is reviewed rather than silently accepted. Do not delete the baseline.
A rule reports nothing at all
Check the selector matches: a contract whose sweep found no elements would "pass" while measuring nothing, so r$ fails loudly instead (a run that performed zero checks). In component-scoped stylesheets — CSS modules, Tailwind hashes — prefer stable hooks (data-testid, roles, landmarks) over generated class names.
The page clearly scrolls sideways but nothing is reported
Fixed in 1.0.0-alpha.0: the check now compares the document's own horizontal reach against the viewport, which catches content overflowing inside a box whose rect looks fine. If you are on an earlier build, upgrade.
Verification results you disagree with
A contrast violation on something invisible
Elements that are present but not visible in the resting state — sr-only clips, opacity: 0, 1×1 absolutes — are excluded from contrast and touch-target checks. If you see one reported, the element is probably visible in a way you did not intend; check it in the browser first.
A touch-target violation on a link inside a paragraph
WCAG 2.5.8 exempts links flowing inline in prose, and so do we. If one is still reported, the link is not inline in text — it is a block-level element that happens to be short.
A contrast pass you do not believe
Contrast is composited against the background actually painted behind the element, walking up through semi-transparent layers. That is deliberately stricter than comparing against the declared background-color, and it is where naive checks report a pass they should not.
Tests
Geometry assertions fail in unit tests
happy-dom and jsdom have no layout: every rect is zero, so whenWraps and friends cannot work there. Unit-test the declarations (what the style map contains, what the stylesheet emits) and put anything that needs real layout in a browser test. See the testing guide.
Still stuck? Open an issue with the output of npx @responsivejs/cli doctor and, if it is a measurement you disagree with, the JSON report (-f json) — it carries the measured numbers, so the disagreement is about facts rather than impressions.