Skip to content

feat(lint): flag CSS rules that select on the inline style attribute - #6261

Open
miguel-heygen wants to merge 2 commits into
mainfrom
dslivers/lint-style-attr-selector
Open

miguel-heygen wants to merge 2 commits into
mainfrom
dslivers/lint-style-attr-selector

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What this fixes

A CSS rule that selects on the inline style attribute ([style], [style*=…], [style^=…] and the other attribute forms) makes every inline style write expensive. GSAP writes inline styles on every animated element every frame, so a composition with such a rule plays slowly in the preview:

  • Chrome restyles the written element, and its whole subtree when the rule reaches descendants ([style*="visibility: hidden"] *). With a few hundred animated elements, that is most of the page on every frame.
  • Safari re-encodes the element's whole inline style on each write once any rule selects on style.

The runtime's own two rules of this kind are removed in #5974. Authored rules of the same shape bring the cost back, and the lint today recommends one: the fix hint for runtime_hidden_style_opacity suggests [data-composition-src][style*="visibility: hidden"] guards.

Changes

  • New rule inline_style_attribute_selector (error): flags any selector that reads the style attribute, including inside :not()/:is() and in any letter case. It reuses the CSS-selector pass and postcss-selector-parser the lint already has, and checks inline <style> blocks and the linked stylesheets the lint already reads. Each selector is reported once. Lint errors block a render only under --strict, as for the other rules.
  • Fix hint (and docs) for runtime_hidden_style_opacity: no longer recommends a [style…] guard. It now says the runtime already hides a hidden clip and its contents, and to animate on the timeline or toggle a class or data attribute instead. The rule's detection is unchanged: it is about the video replacement frame, and a host-scoped guard does not affect that. The new rule flags such guards for their playback cost.
  • docs/reference/lint-rules.mdx: the new rule, and the corrected fix.

Tests

packages/lint/src/rules/core.test.ts:

  • Each selector form errors: [style], *=, ^=, $=, ~=, |=, uppercase with the i flag, and inside :not().
  • Lookalikes pass: a class, [data-state], [data-style], [styles], [data-hf-style].
  • A selector repeated across rules is reported once, and each selector of a list on its own.
  • The runtime_hidden_style_opacity fix hint contains no [style.

On main the new tests fail (10 of them). Each was checked by breaking the rule:

  • Matching only lowercase [style*=…] turns the other forms red.
  • Skipping attributes nested in a pseudo-class turns the :not() case red.
  • Matching any attribute that contains "style" turns the lookalikes red.

The whole lint suite passes three runs in a row. The lint package typecheck, bun run lint, oxfmt and the fallow audit on the changed files are clean.

@mintlify

mintlify Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 25, 2026, 12:56 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 142 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqheldtwice-tlhold-px-r0-nested-z100: tracking 120.29, pressJump 312.3, drop 0.07, reload 0.07, render 0.03, renderKey -, undo true, teleport false / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 1 nit · head b986bf81

  • nit packages/lint/src/rules/core.ts:764 — a selector that trips runtime_hidden_style_opacity (e.g. [style*="visibility: hidden"] { opacity: 0 }) now produces two errors for the same rule. Both give the same fix, so this is only noise. Skip the new finding when the hidden-style one fires if you want a single error.
Scope

Read the full diff. selectsOnInlineStyle walks every attribute node, including those nested in :not()/:is(), and compares names case-insensitively. It runs over resolvedRuleSelectors, which covers inline and linked stylesheets. The runtime's own [style*=…] rules (timedClipHide.ts, pending #5974) are injected by JS and never linted. No skill, template, or doc in the repo still recommends a [style…] guard. The compiler's static guard only warns on errors, so the new rule blocks a render only under --strict. CI passed at this head.

This branch was successfully deployed

1 active deployment
staging - docs — b986bf81 Deployed Oct 25, 2026 by mintlify[bot]
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