Environment
ESLint version: v10.8.0
@eslint/css version: v1.4.0
Node version: v22.22.3
npm version: v10.8.2
Operating System: StackBlitz WebContainer (linux)
Which language are you using?
stylesheet
What did you do?
// Configuration
// eslint.config.js
import css from "@eslint/css";
export default [
{
files: ["**/*.css"],
plugins: { css },
language: "css/css",
rules: { "css/font-family-fallbacks": "error" },
},
];
a { font-family: Arial, Sans-Serif; }
b { font-family: Arial, SERIF; }
c { font-family: Serif; }
d { font: 16px Arial, MONOSPACE; }
/* control cases, correctly not reported */
e { font-family: Arial, sans-serif; }
f { font-family: serif; }
What did you expect to happen?
I expected no errors to be reported for any of the six declarations above.
Generic family keywords are pre-defined CSS keywords, and the CSS specification states that keywords are matched ASCII case-insensitively. From CSS Values and Units Module Level 4, §4.1 Pre-defined Keywords:
(Keywords are identifiers and are interpreted ASCII case-insensitively (i.e., [a-z] and [A-Z] are equivalent))
Generic families are keywords only when they are unquoted — CSS Fonts Module Level 4 notes that <generic-font-family> keywords cannot be quoted, otherwise they are interpreted as a <font-family-name>. All four cases above are unquoted, so they are the keyword form.
So Sans-Serif, SERIF and MONOSPACE are valid generic font families and mean exactly the same thing as their lowercase forms. Only the lowercase forms are accepted right now.
What actually happened?
The four declarations that use uppercase letters are reported, while the two lowercase control cases are not:
❯ npx eslint .
/home/projects/stackblitz-starters-adhqdqr2/test.css
1:18 error Use a generic font last css/font-family-fallbacks
2:18 error Use a generic font last css/font-family-fallbacks
3:18 error Use fallback fonts and a generic font last css/font-family-fallbacks
4:11 error Use a generic font last css/font-family-fallbacks
✖ 4 problems (4 errors, 0 warnings)
The fact that only the uppercase variants are reported shows that the cause is case-sensitive matching.
Link to Minimal Reproducible Example
https://stackblitz.com/edit/stackblitz-starters-adhqdqr2
Participation
AI acknowledgment
Additional comments
Hello. I looked into where this comes from, and I would like to share what I found.
Where the bug is
In src/rules/font-family-fallbacks.js, the genericFonts set is defined entirely in lowercase (L20–L34), and the candidate tokens are compared against it without any case normalization:
| Line |
Code |
| L101 |
!genericFonts.has(valueList.at(-1)) |
| L183 / L189 |
genericFonts.has(variableList[...]) |
| L198 |
genericFonts.has(valueArr[0].name) |
| L261 |
genericFonts.has(fontsList.at(-1)) |
| L273 |
genericFonts.has(lastFont.name) |
| L356 / L389 / L400 / L444 |
genericFonts.has(...) |
What makes me think this is an oversight rather than an intentional decision is that the same file already normalizes case for the other kind of keyword it handles. CSS-wide keywords are lowercased in two places:
// L43
return cssWideKeywords.has(value.trim().toLowerCase());
// L135, inside create()
sourceCode.lexer.cssWideKeywords.map(keyword => keyword.toLowerCase())
So CSS-wide keywords are treated case-insensitively, but generic font keywords in the very same rule are not.
Precedent
While searching for duplicates I found that this repository has fixed this same class of bug already:
font-family-fallbacks looks like the one rule that was left out. I could not find an existing issue or pull request covering this, so I am reporting it.
If this is accepted, I'd like to take it on, following the same approach as #222.
Environment
ESLint version: v10.8.0
@eslint/css version: v1.4.0
Node version: v22.22.3
npm version: v10.8.2
Operating System: StackBlitz WebContainer (linux)
Which language are you using?
stylesheet
What did you do?
What did you expect to happen?
I expected no errors to be reported for any of the six declarations above.
Generic family keywords are pre-defined CSS keywords, and the CSS specification states that keywords are matched ASCII case-insensitively. From CSS Values and Units Module Level 4, §4.1 Pre-defined Keywords:
(Keywords are identifiers and are interpreted ASCII case-insensitively (i.e., [a-z] and [A-Z] are equivalent))
Generic families are keywords only when they are unquoted — CSS Fonts Module Level 4 notes that
<generic-font-family>keywords cannot be quoted, otherwise they are interpreted as a<font-family-name>. All four cases above are unquoted, so they are the keyword form.So
Sans-Serif,SERIFandMONOSPACEare valid generic font families and mean exactly the same thing as their lowercase forms. Only the lowercase forms are accepted right now.What actually happened?
The four declarations that use uppercase letters are reported, while the two lowercase control cases are not:
The fact that only the uppercase variants are reported shows that the cause is case-sensitive matching.
Link to Minimal Reproducible Example
https://stackblitz.com/edit/stackblitz-starters-adhqdqr2
Participation
AI acknowledgment
Additional comments
Hello. I looked into where this comes from, and I would like to share what I found.
Where the bug is
In
src/rules/font-family-fallbacks.js, thegenericFontsset is defined entirely in lowercase (L20–L34), and the candidate tokens are compared against it without any case normalization:!genericFonts.has(valueList.at(-1))genericFonts.has(variableList[...])genericFonts.has(valueArr[0].name)genericFonts.has(fontsList.at(-1))genericFonts.has(lastFont.name)genericFonts.has(...)What makes me think this is an oversight rather than an intentional decision is that the same file already normalizes case for the other kind of keyword it handles. CSS-wide keywords are lowercased in two places:
So CSS-wide keywords are treated case-insensitively, but generic font keywords in the very same rule are not.
Precedent
While searching for duplicates I found that this repository has fixed this same class of bug already:
fix: make relative-font-units unit matching case-insensitivefont-family-fallbackslooks like the one rule that was left out. I could not find an existing issue or pull request covering this, so I am reporting it.If this is accepted, I'd like to take it on, following the same approach as #222.