From 60a425193c643665c99dfd7e0d5cea5b052127a9 Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:54:15 -0800 Subject: [PATCH] fix: treat CSS attribute selectors as case-insensitive for HTML enumerated attributes (#17712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #17207 CSS attribute selectors for HTML enumerated attributes (like `method`, `type`, `dir`, etc.) are supposed to match case-insensitively per the HTML spec. Browsers handle this correctly — `form[method="get"]` matches `
`. But Svelte's CSS pruning was doing a strict case-sensitive comparison, which meant: 1. The selector got incorrectly flagged as unused (no `css_unused_selector` warning was shown when spreads were involved, but the selector was still pruned) 2. The scoping class wasn't applied to the matching element 3. Styles silently disappeared in production builds The fix adds a set of known HTML attributes with case-insensitive enumerated values (sourced from the HTML spec) and uses it during CSS attribute selector matching. The explicit CSS `s` flag still overrides this behavior, as expected. ### Before ```svelte

Hello

``` ### After The selector correctly matches and styles are applied. ### Test plan - Added `attribute-selector-html-case-insensitive` CSS test covering `form[method]` and `input[type]` cases - All 179 existing CSS tests pass - Verified the existing `attribute-selector-case-sensitive` test (using `s` flag) still works correctly - Compiler error tests and validator tests all pass --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/css-attribute-case-insensitive.md | 5 ++ .../phases/2-analyze/css/css-prune.js | 48 ++++++++++++++++++- .../expected.css | 11 +++++ .../expected.html | 1 + .../input.svelte | 23 +++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/css-attribute-case-insensitive.md create mode 100644 packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.css create mode 100644 packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.html create mode 100644 packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/input.svelte diff --git a/.changeset/css-attribute-case-insensitive.md b/.changeset/css-attribute-case-insensitive.md new file mode 100644 index 0000000000..e5b3bcea2b --- /dev/null +++ b/.changeset/css-attribute-case-insensitive.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat CSS attribute selectors as case-insensitive for HTML enumerated attributes diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 7242c69d8c..7e05d2e7d3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -22,6 +22,50 @@ const whitelist_attribute_selector = new Map([ ['dialog', ['open']] ]); +/** + * HTML attributes whose enumerated values are case-insensitive per the HTML spec. + * CSS attribute selectors match these values case-insensitively in HTML documents. + * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors HTML spec} + */ +const case_insensitive_attributes = new Set([ + 'accept-charset', + 'autocapitalize', + 'autocomplete', + 'behavior', + 'charset', + 'crossorigin', + 'decoding', + 'dir', + 'direction', + 'draggable', + 'enctype', + 'enterkeyhint', + 'fetchpriority', + 'formenctype', + 'formmethod', + 'formtarget', + 'hidden', + 'http-equiv', + 'inputmode', + 'kind', + 'loading', + 'method', + 'preload', + 'referrerpolicy', + 'rel', + 'rev', + 'role', + 'rules', + 'scope', + 'shape', + 'spellcheck', + 'target', + 'translate', + 'type', + 'valign', + 'wrap' +]); + /** @type {Compiler.AST.CSS.Combinator} */ const descendant_combinator = { type: 'Combinator', @@ -523,7 +567,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, selector.name, selector.value && unquote(selector.value), selector.matcher, - selector.flags?.includes('i') ?? false + (selector.flags?.includes('i') ?? false) || + (!selector.flags?.includes('s') && + case_insensitive_attributes.has(selector.name.toLowerCase())) ) ) { return false; diff --git a/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.css b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.css new file mode 100644 index 0000000000..01ec1b2269 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.css @@ -0,0 +1,11 @@ + form[method="get"].svelte-xyz h1:where(.svelte-xyz) { + color: red; + } + + form[method="post"].svelte-xyz h1:where(.svelte-xyz) { + color: blue; + } + + input[type="text"].svelte-xyz { + color: green; + } diff --git a/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.html b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.html new file mode 100644 index 0000000000..101c03f836 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/expected.html @@ -0,0 +1 @@ +

Hello

World

diff --git a/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/input.svelte b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/input.svelte new file mode 100644 index 0000000000..e11a922d75 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-html-case-insensitive/input.svelte @@ -0,0 +1,23 @@ +
+

Hello

+
+ +
+

World

+
+ + + +