Skip to content

Commit ce256da

Browse files
feat: add relative-font-units rule (#133)
1 parent 0d61035 commit ce256da

File tree

5 files changed

+1216
-10
lines changed

5 files changed

+1216
-10
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,17 @@ export default defineConfig([
6565

6666
<!-- Rule Table Start -->
6767

68-
| **Rule Name** | **Description** | **Recommended** |
69-
| :----------------------------------------------------------------------- | :------------------------------------ | :-------------: |
70-
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71-
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72-
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73-
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
74-
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
75-
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
76-
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
77-
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
68+
| **Rule Name** | **Description** | **Recommended** |
69+
| :----------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70+
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71+
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72+
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
74+
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
75+
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
76+
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
77+
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
78+
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
7879

7980
<!-- Rule Table End -->
8081

docs/rules/relative-font-units.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# relative-font-units
2+
3+
Enforce the use of relative units for font size.
4+
5+
## Background
6+
7+
The `font-size` property in CSS defines the size of the text. It can be set using:
8+
9+
1. Keywords (e.g., `small`, `medium`, `large`).
10+
1. Length units (e.g., `px`, `em`, `rem`, `pt`).
11+
1. Percentages (`%`, relative to the parent element's font size).
12+
13+
Generally, relative units such as `rem` or `em` are preferred over absolute ones like `px`, `pt` because of the following reasons:
14+
15+
- **Responsive Design** - Relative units adapt better to various screen widths and pixel densities (e.g., mobile vs. desktop).
16+
- **Accessibility** - Relative units allow text to scale when users adjust browser settings or zoom levels.
17+
- **Consistency and Scalability** - Using relative units allow for consistent scaling across a whole site.
18+
- **Maintainability and Reusability** - Relative units allow components or utility classes to work well in different contexts.
19+
20+
## Rule Details
21+
22+
This rule enforces the use of relative units for font size.
23+
24+
## Options
25+
26+
This rule accepts an option which is an object with the following property:
27+
28+
- `allowUnits` (default: `["rem"]`) - Specify an array of relative units that are allowed to be used. You can use the following units:
29+
30+
- **%**: Represents the "percentage" of the parent element’s font size, allowing the text to scale relative to its container.
31+
- **cap**: Represents the "cap height" (nominal height of capital letters) of the element's font.
32+
- **ch**: Represents the width or advance measure of the "0" glyph in the element's font.
33+
- **em**: Represents the calculated font-size of the element.
34+
- **ex**: Represents the x-height of the element's font.
35+
- **ic**: Equal to the advance measure of the "水" glyph (CJK water ideograph) in the font.
36+
- **lh**: Equal to the computed line-height of the element.
37+
- **rcap**: Equal to the "cap height" of the root element's font.
38+
- **rch**: Equal to the width or advance measure of the "0" glyph in the root element's font.
39+
- **rem**: Represents the font-size of the root element.
40+
- **rex**: Represents the x-height of the root element's font.
41+
- **ric**: Equal to the ic unit on the root element's font.
42+
- **rlh**: Equal to the lh unit on the root element's font.
43+
44+
Example of **incorrect** code for default `{ allowUnits: ["rem"] }` option:
45+
46+
```css
47+
/* eslint css/relative-font-units: ["error", { allowUnits: ["rem"] }] */
48+
49+
a {
50+
font-size: 10px;
51+
}
52+
53+
b {
54+
font-size: 2em;
55+
}
56+
57+
c {
58+
font-size: small;
59+
}
60+
```
61+
62+
Example of **correct** code for default `{ allowUnits: ["rem"] }` option:
63+
64+
```css
65+
/* eslint css/relative-font-units: ["error", { allowUnits: ["rem"] }] */
66+
67+
a {
68+
font-size: 2rem;
69+
}
70+
71+
b {
72+
font-size: 1rem;
73+
width: 20px;
74+
}
75+
76+
c {
77+
font-size: var(--foo);
78+
}
79+
80+
d {
81+
font-size: calc(2rem + 2px);
82+
}
83+
```
84+
85+
Font size can also be specified in `font` property:
86+
87+
Example of **correct** code for `{ allowUnits: ["em", "%"] }` option:
88+
89+
```css
90+
/* eslint css/relative-font-units: ["error", { allowUnits: ["em", "%"] }] */
91+
92+
a {
93+
font-size: 2em;
94+
}
95+
96+
b {
97+
font:
98+
20% Arial,
99+
sans-serif;
100+
}
101+
102+
c {
103+
font: Arial var(--foo);
104+
}
105+
```
106+
107+
## When Not to Use It
108+
109+
If your project does not prioritize the use of relative font-size units—such as in cases requiring absolute sizing for print styles, pixel-perfect UI components, or embedded widgets—you may safely disable this rule without impacting your intended design precision.
110+
111+
## Further Reading
112+
113+
- [`Surprising truth about Pixels and Accessibility`](https://www.joshwcomeau.com/css/surprising-truth-about-pixels-and-accessibility/)

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import noImportant from "./rules/no-important.js";
1515
import noInvalidProperties from "./rules/no-invalid-properties.js";
1616
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
1717
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
18+
import relativeFontUnits from "./rules/relative-font-units.js";
1819
import useLayers from "./rules/use-layers.js";
1920
import useBaseline from "./rules/use-baseline.js";
2021

@@ -37,6 +38,7 @@ const plugin = {
3738
"no-invalid-at-rules": noInvalidAtRules,
3839
"no-invalid-properties": noInvalidProperties,
3940
"prefer-logical-properties": preferLogicalProperties,
41+
"relative-font-units": relativeFontUnits,
4042
"use-layers": useLayers,
4143
"use-baseline": useBaseline,
4244
},

src/rules/relative-font-units.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* @fileoverview Enforce the use of relative units for font size.
3+
* @author Tanuj Kanti
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @import { CSSRuleDefinition } from "../types.js"
12+
* @typedef {"allowedFontUnits"} RelativeFontUnitsMessageIds
13+
* @typedef {[{allowUnits?: string[]}]} RelativeFontUnitsOptions
14+
* @typedef {CSSRuleDefinition<{ RuleOptions: RelativeFontUnitsOptions, MessageIds: RelativeFontUnitsMessageIds}>} RelativeFontUnitsRuleDefinition
15+
*/
16+
17+
//-----------------------------------------------------------------------------
18+
// Helpers
19+
//-----------------------------------------------------------------------------
20+
21+
const relativeFontUnits = [
22+
"%",
23+
"cap",
24+
"ch",
25+
"em",
26+
"ex",
27+
"ic",
28+
"lh",
29+
"rcap",
30+
"rch",
31+
"rem",
32+
"rex",
33+
"ric",
34+
"rlh",
35+
];
36+
37+
const fontSizeIdentifiers = new Set([
38+
"xx-small",
39+
"x-small",
40+
"small",
41+
"medium",
42+
"large",
43+
"x-large",
44+
"xx-large",
45+
"xxx-large",
46+
"smaller",
47+
"larger",
48+
"math",
49+
"inherit",
50+
"initial",
51+
"revert",
52+
"revert-layer",
53+
"unset",
54+
]);
55+
56+
//-----------------------------------------------------------------------------
57+
// Rule Definition
58+
//-----------------------------------------------------------------------------
59+
60+
/** @type {RelativeFontUnitsRuleDefinition} */
61+
export default {
62+
meta: {
63+
type: "suggestion",
64+
65+
docs: {
66+
description: "Enforce the use of relative font units",
67+
recommended: false,
68+
url: "https://github.com/eslint/css/blob/main/docs/rules/relative-font-units.md",
69+
},
70+
71+
schema: [
72+
{
73+
type: "object",
74+
properties: {
75+
allowUnits: {
76+
type: "array",
77+
items: {
78+
enum: relativeFontUnits,
79+
uniqueItems: true,
80+
},
81+
},
82+
},
83+
},
84+
],
85+
86+
defaultOptions: [
87+
{
88+
allowUnits: ["rem"],
89+
},
90+
],
91+
92+
messages: {
93+
allowedFontUnits:
94+
"Use only allowed relative units for 'font-size' - {{allowedFontUnits}}.",
95+
},
96+
},
97+
98+
create(context) {
99+
const [{ allowUnits: allowedFontUnits }] = context.options;
100+
101+
return {
102+
Declaration(node) {
103+
if (node.property === "font-size") {
104+
if (
105+
node.value.type === "Value" &&
106+
node.value.children.length > 0
107+
) {
108+
const value = node.value.children[0];
109+
110+
if (
111+
(value.type === "Dimension" &&
112+
!allowedFontUnits.includes(value.unit)) ||
113+
value.type === "Identifier" ||
114+
(value.type === "Percentage" &&
115+
!allowedFontUnits.includes("%"))
116+
) {
117+
context.report({
118+
loc: value.loc,
119+
messageId: "allowedFontUnits",
120+
data: {
121+
allowedFontUnits:
122+
allowedFontUnits.join(", "),
123+
},
124+
});
125+
}
126+
}
127+
}
128+
129+
if (node.property === "font") {
130+
if (
131+
node.value.type === "Value" &&
132+
node.value.children.length > 0
133+
) {
134+
const value = node.value;
135+
136+
const dimensionNode = value.children.find(
137+
child => child.type === "Dimension",
138+
);
139+
const identifierNode = value.children.find(
140+
child =>
141+
child.type === "Identifier" &&
142+
fontSizeIdentifiers.has(child.name),
143+
);
144+
const percentageNode = value.children.find(
145+
child => child.type === "Percentage",
146+
);
147+
let location;
148+
let shouldReport = false;
149+
150+
const conditions = [
151+
{
152+
check:
153+
!allowedFontUnits.includes("%") &&
154+
percentageNode,
155+
loc: percentageNode?.loc,
156+
},
157+
{
158+
check: identifierNode,
159+
loc: identifierNode?.loc,
160+
},
161+
{
162+
check:
163+
dimensionNode &&
164+
!allowedFontUnits.includes(
165+
dimensionNode.unit,
166+
),
167+
loc: dimensionNode?.loc,
168+
},
169+
];
170+
for (const condition of conditions) {
171+
if (condition.check) {
172+
shouldReport = true;
173+
location = condition.loc;
174+
break;
175+
}
176+
}
177+
178+
if (shouldReport) {
179+
context.report({
180+
loc: location,
181+
messageId: "allowedFontUnits",
182+
data: {
183+
allowedFontUnits:
184+
allowedFontUnits.join(", "),
185+
},
186+
});
187+
}
188+
}
189+
}
190+
},
191+
};
192+
},
193+
};

0 commit comments

Comments
 (0)