or... "How the @stylexjs/sort-keys rule silently breaks responsive styles"
The Setup: Following All the Rules
After solving the StyleX + Vite "Invalid empty selector" mystery, I thought I had StyleX figured out. I was following every documented best practice:
Vite config - using enableMediaQueryOrder: true as recommended in v0.15.0:
// vite.config.js
styleXUnplugin.vite({
enableMediaQueryOrder: true,
})
ESLint config - using the recommended StyleX plugin rules:
// eslint.config.mjs
rules: {
"@stylexjs/valid-styles": "error",
"@stylexjs/sort-keys": "warn", // Keep styles organized!
}
Responsive styles - using inline strings (learned that lesson) with correct cascade order:
const styles = stylex.create({
container: {
padding: {
default: "2rem",
"@media (max-width: 768px)": "1.5rem", // tablet
"@media (max-width: 640px)": "1rem", // mobile
},
},
});
Everything looked correct. The order was right—larger breakpoints first so smaller ones can override them in the CSS cascade.
The Problem: A Helpful ESLint Warning
Then I ran npm run lint and saw:
warning StyleX property key "@media (max-width: 640px)"
should be above "@media (max-width: 768px)"
Being a good developer who trusts their linter, I ran npm run lint -- --fix. ESLint helpfully "fixed" my code:
padding: {
default: "2rem",
"@media (max-width: 640px)": "1rem", // Now first (640 < 768 alphabetically)
"@media (max-width: 768px)": "1.5rem", // Now last
},
The Breakage: Mobile Gets Tablet Styles
Here's what happens on a 500px mobile screen:
- Both
max-width: 640pxandmax-width: 768pxmatch (500 < 640 < 768) - With
enableMediaQueryOrder: true, StyleX makes "later source order wins" - After ESLint sorting,
768pxis later in source - Result: Mobile devices get
1.5reminstead of1rem
The responsive design is now broken, and ESLint told me to do it.
Why This Happens
The @stylexjs/sort-keys rule sorts all object keys alphabetically. For most properties, this is fine—it keeps your styles organized and consistent.
But media query strings sort by their numeric values as strings:
-
"@media (max-width: 640px)"→ "640" comes first -
"@media (max-width: 768px)"→ "768" comes second
For max-width queries, this is exactly backwards. CSS cascade requires:
- Larger breakpoints first (768px)
- Smaller breakpoints last (640px) to override
The ESLint rule doesn't understand CSS cascade semantics—it just sorts alphabetically.
The Irony
The StyleX team added enableMediaQueryOrder specifically to handle media query ordering:
"You can now write overlapping media queries in the order you desire, and the compiler will rewrite them so that later queries take precedence over earlier ones."
— StyleX v0.15.0 Release Notes
This feature relies on source order. But the StyleX ESLint plugin's sort-keys rule destroys that source order. The left hand doesn't know what the right hand is doing.
The Workaround
Until this is fixed upstream, you need ESLint disable blocks around responsive properties:
const styles = stylex.create({
container: {
/* eslint-disable @stylexjs/sort-keys -- max-width cascade: larger breakpoints first */
padding: {
default: "2rem",
"@media (max-width: 768px)": "1.5rem",
"@media (max-width: 640px)": "1rem",
},
/* eslint-enable @stylexjs/sort-keys */
},
});
The disable/enable block pattern works because:
-
// eslint-disable-next-lineonly affects the next line - The warning is reported on the nested keys, not the property line
- Block comments cover everything between them
Key Takeaways
Don't blindly trust
--fix— Auto-fixes can break semantic ordering that linters don't understandTest responsive styles after linting — If you run
eslint --fix, verify your breakpoints still workDocument your exceptions — The
-- max-width cascade: larger breakpoints firstcomment explains WHY the rule is disabledFile upstream issues — I've filed an issue requesting the sort-keys rule be media-query-aware
The Fix We Need
The @stylexjs/sort-keys rule should either:
-
Skip sorting inside media query objects — Detect
@mediakeys and preserve author order -
Sort by specificity — Understand that
max-widthneeds descending order,min-widthneeds ascending - Provide configuration — Let users opt-out for specific patterns
Until then, keep those ESLint disable comments handy.
Related Posts:
GitHub Issue:
Top comments (0)