Skip to content

Round-trip csstruth's own selectors, and tighten the invariants - #2

Merged
ikraamg merged 6 commits into
mainfrom
overlap-container-escape
Jul 19, 2026
Merged

Round-trip csstruth's own selectors, and tighten the invariants#2
ikraamg merged 6 commits into
mainfrom
overlap-container-escape

Conversation

@ikraamg

@ikraamg ikraamg commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Five improvements that came out of actually running csstruth against real Tailwind pages (trmnl.com) and hitting friction the tool itself created. The two that bit most are self-inconsistencies: csstruth printed selectors it couldn't consume, and layout --selector matched differently than inspect/explain did.

  • adds selector escaping so inspect/explain accept csstruth's own output — Tailwind variant/arbitrary-value classes (md:flex, w-[calc(100%-2rem)], data-[state=open]:flex) now round-trip instead of erroring. Retry only on a miss, so a valid selector is never disturbed
  • makes layout --selector a compound matcher (tag + #id + class subset), so nav (bare tag on a classed element) and div.card.featured (a subset of an element with >3 classes) resolve like they already do in inspect/explain. Falls back to the old exact/raw contract for anything it can't tokenize, so a dotted or #-bearing arbitrary value (.gap-[0.5rem], .bg-[#fff]) still resolves
  • extends the overlap descendant-escape suppression and parent-bleed to the vertical axis — a list overflowing its card and lapping the card below is reported as the child bleeding its own container, not miscast as the innocent cousin's overlap
  • exempts deliberately-displaced children (negative margin, a translating transform, positioned with a real inset offset) from parent-bleed. Only a genuine 2D translation counts, so translateZ(0) and other GPU-compositing no-ops still flag a real overflow
  • adds the WCAG 2.5.8 inline exception to the tap-target check — a text link inside a sentence is constrained by the surrounding line-height and exempt, while an icon-only link or a standalone small link still flags

Every exemption above was tightened by an adversarial review that found a real hidden bug the RED/GREEN tests passed clean over: translateZ(0) masking a real overflow, a dotted-class scoping regression, and an icon-only link plus an appearance:none input slipping through the tap-target exemption. Those are the parts worth the closest look.

Validated end-to-end against live trmnl.com — the Tailwind selectors round-trip, layout --selector nav scopes, and inline prose links stop nagging while standalone/icon links still flag. The displacement exemption has no natural repro on that page (the right sign for an exemption); it stays fixture + review covered.

Full test suite green — the one intermittent failure is the documented macOS Chrome-shutdown lifecycle test, which passes in isolation.

Todo

  • cut a release once merged (minor bump — these are additive)

ikraamg and others added 6 commits July 19, 2026 08:10
Necessary to stop the overlap check from blaming an innocent cousin when
a static child overflows its own container and laps into the neighbor's
box. parent-bleed already reports the escaper bleeding its container on
the horizontal axis, so the cousin-overlap is redundant noise on a real
app dashboard (a list overflowing its card, a wide control spilling a
cell). Suppressing it never hides a defect: the escape geometry is
impossible without a horizontal parent-bleed at some level between the
node and the container, which that rule always surfaces.

Scoped to the horizontal axis on purpose. A vertical-only escape is not
yet covered by parent-bleed, so it stays flagged — a silently dropped
check would be worse than a false positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Necessary to make the overlap and parent-bleed rules honest on both axes.

parent-bleed now measures the vertical axis (a child taller than a
height-constrained container), mirroring the existing horizontal check
and clamped by the nearest overflow-y the same way. That makes it safe
to extend the overlap descendant-escape suppression to vertical: a list
overflowing its card and lapping the card below is the escaper's bleed,
not the innocent cousin's overlap, and parent-bleed now surfaces the
real defect on either axis.

Extending parent-bleed to vertical surfaced a pre-existing philosophy
gap: it flagged deliberately displaced children (a negative-margin
avatar over a card, a positioned nudge), more visibly upward/downward.
parent-bleed now exempts a child moved out of its box on purpose — a
translating transform, a negative margin, or a non-static position with
a real inset offset. Only a genuine 2D translation counts: translateZ(0)
and other GPU-compositing no-ops resolve to zero translation and stay
flagged, so a real overflow under them is never silently dropped. A bare
position:relative with no offset stays in normal flow and still bleeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Necessary because selectorOf emits raw class names and Tailwind's variant
and arbitrary-value classes (md:flex, w-[calc(100%-2rem)],
data-[state=open]:flex, content-['x']) carry characters querySelector
reads as syntax. csstruth printed those selectors in every layout/check
line but resolveNode and inspect could not consume them — the tool's
output did not round-trip back into the tool, forcing a backendNodeId
detour.

resolveNode now retries with an escaped selector after the raw one fails
to match, and inspect's browser probe falls back the same way. The escape
is an allowlist — every character that is not an identifier or structural
selector char — so a new Tailwind offender never has to be enumerated by
hand. Retry only fires on a miss, so a selector that matches raw is left
untouched. Inner-dot arbitrary values and #-in-brackets still collide with
the class and id separators and need a backendNodeId.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Necessary so layout --selector behaves like the intuition inspect and
explain already give. findNode matched only the exact rendered selectorOf
string, a bare #id, or a single class, so `layout --selector nav` on a
classed element and `--selector div.card.featured` (a subset of an
element with more than three classes) both silently returned nothing.

findNode now parses a simple compound selector (tag + #id + class subset)
and matches against the node's classes directly, so Tailwind classes need
no escaping here. Anything the parser can't tokenize — combinators,
attribute or pseudo selectors, or a dotted/#-bearing arbitrary value like
.gap-[0.5rem] or .bg-[#fff] — falls back to the raw selectorOf/#id/.class
shorthands the old contract matched, so the change is purely additive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Necessary to stop flagging a text link inside a sentence as a too-small
tap target. Such a link's height is constrained by the surrounding
line-height — 24px is unreachable without reflowing the prose — which is
exactly the case WCAG 2.5.8 exempts, so reporting it is noise on any
content-heavy page.

The exemption is narrow so it never hides a real bug: the element must
carry its own text (an icon-only link is not line-height-constrained — a
16px icon can grow to 24px without reflow — so it still flags), be
display:inline, and sit in a parent that has real text around it. Inputs
are excluded and keep routing through the label-measurement path, since
an appearance:none input can itself compute display:inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLAUDE.md gives contributors the full invariant-rule set and the field-tag
convention so the rationale for each exemption is one grep away. The skill's
new Selectors section tells users csstruth's own output round-trips straight
back in — Tailwind variant and arbitrary-value classes included — and that
layout scoping now takes a bare tag or a class subset, and folds the built-in
exemptions into "Reading violations" so nobody re-investigates a handled case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ikraamg
ikraamg merged commit 3ab7f39 into main Jul 19, 2026
2 checks passed
@ikraamg
ikraamg deleted the overlap-container-escape branch July 19, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant