https://github.com/yuschick/stylelint-plugin-defensive-css
A Stylelint plugin for enforcing defensive CSS best practices.
https://github.com/yuschick/stylelint-plugin-defensive-css
css defensive plugin stylelint stylelint-plugin
Last synced: 4 months ago
JSON representation
A Stylelint plugin for enforcing defensive CSS best practices.
- Host: GitHub
- URL: https://github.com/yuschick/stylelint-plugin-defensive-css
- Owner: yuschick
- License: mit
- Created: 2023-03-22T17:02:03.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2026-02-03T18:18:30.000Z (5 months ago)
- Last Synced: 2026-02-03T21:03:40.848Z (5 months ago)
- Topics: css, defensive, plugin, stylelint, stylelint-plugin
- Language: TypeScript
- Homepage: https://defensivecss.dev
- Size: 687 KB
- Stars: 147
- Watchers: 3
- Forks: 7
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Codeowners: CODEOWNERS
Awesome Lists containing this project
README






A Stylelint plugin to enforce [Defensive CSS](https://defensivecss.dev/) best practices.
> [!TIP]
> [V1 documentation can be found here](./V1-DOCUMENTATION.md)
## Table of Contents
[Getting Started](#getting-started) | [Quickstart](#quickstart) | [Plugin Configs](#defensive-css-configs) | [Plugin Rules](#defensive-css-rules) | [Troubleshooting](#troubleshooting)
## Getting Started
> [!IMPORTANT]
> The plugin requires [Stylelint](https://stylelint.io/) v14.0.0 or greater.
To get started using the plugin, it must first be installed.
```bash
npm i stylelint-plugin-defensive-css --save-dev
```
```bash
yarn add stylelint-plugin-defensive-css --dev
```
With the plugin installed, it must be added to the `plugins` array of your Stylelint config.
```json
{
"plugins": ["stylelint-plugin-defensive-css"],
}
```
After adding the plugin to the configuration file, you now have access to the various rules and options it provides.
## Quickstart
After installation, add this to your `.stylelintrc.json`:
```json
{
"plugins": ["stylelint-plugin-defensive-css"],
"extends": ["stylelint-plugin-defensive-css/configs/recommended"]
}
```
## Defensive CSS Configs
For quick setup, the plugin provides preset configurations that enable commonly used rules.
### Recommended
The `recommended` preset enables core defensive CSS rules with sensible defaults, suitable for most projects.
**Usage:**
```json
{
"extends": ["stylelint-plugin-defensive-css/configs/recommended"]
}
```
**Equivalent to:**
```json
{
"plugins": ["stylelint-plugin-defensive-css"],
"rules": {
"defensive-css/no-accidental-hover": [true, { "severity": "error" }],
"defensive-css/no-list-style-none": [true, { "fix": true, "severity": "error" }],
"defensive-css/no-mixed-vendor-prefixes": [true, { "severity": "error" }],
"defensive-css/no-unsafe-will-change": [true, { "severity": "error" }],
"defensive-css/require-background-repeat": [true, { "severity": "error" }],
"defensive-css/require-dynamic-viewport-height": [true, { "severity": "warning" }],
"defensive-css/require-flex-wrap": [true, { "severity": "error" }],
"defensive-css/require-focus-visible": [true, { "severity": "error" }],
"defensive-css/require-named-grid-lines": [
true,
{ "columns": [true, { "severity": "error" }] },
{ "rows": [true, { "severity": "warning" }] },
],
"defensive-css/require-prefers-reduced-motion": [true, { "severity": "error" }],
}
}
```
### Accessibility
The `accessibility` preset enables accessibility-focused rules to catch common issues that impact keyboard navigation, screen readers, and user preferences.
**Usage:**
```json
{
"extends": ["stylelint-plugin-defensive-css/configs/accessibility"]
}
```
**Equivalent to:**
```json
{
"plugins": ["stylelint-plugin-defensive-css"],
"rules": {
"defensive-css/no-accidental-hover": [true, { "severity": "error" }],
"defensive-css/no-list-style-none": [true, { "fix": true, "severity": "error" }],
"defensive-css/require-focus-visible": [true, { "severity": "error" }],
"defensive-css/require-prefers-reduced-motion": [true, { "severity": "error" }],
},
}
```
### Strict
The `strict` preset enables every rule for the most strict linting offered by the plugin.
**Usage:**
```json
{
"extends": ["stylelint-plugin-defensive-css/configs/strict"]
}
```
## Defensive CSS Rules
The plugin provides multiple rules that can be toggled on and off as needed.
1. [No Accidental Hover](#no-accidental-hover)
2. [No Fixed Sizes](#no-fixed-sizes)
3. [No List Style None](#no-list-style-none)
4. [No Mixed Vendor Prefixes](#no-mixed-vendor-prefixes)
5. [No Unsafe Will-Change](#no-unsafe-will-change)
6. [Require At Layer](#require-at-layer)
7. [Require Background Repeat](#require-background-repeat)
8. [Require Custom Property Fallback](#require-custom-property-fallback)
9. [Require Dynamic Viewport Height](#require-dynamic-viewport-height)
10. [Require Flex Wrap](#require-flex-wrap)
11. [Require Focus Visible](#require-focus-visible)
12. [Require Named Grid Lines](#require-named-grid-lines)
13. [Require Overscroll Behavior](#require-overscroll-behavior)
14. [Require Prefers Reduced Motion](#require-prefers-reduced-motion)
15. [Require Scrollbar Gutter](#require-scrollbar-gutter)
---
### No Accidental Hover
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/hover-media/)
Hover effects indicate interactivity on devices with mouse or trackpad input. However, on touch devices, hover states can cause confusing user experiences where elements become stuck in a hovered state after being tapped, or trigger unintended actions.
**Enable this rule to:** Require all `:hover` selectors to be wrapped in `@media (hover: hover)` queries, ensuring hover effects only apply in supported contexts.
```json
{
"rules": {
"defensive-css/no-accidental-hover": true,
}
}
```
#### No Accidental Hover Examples
✅ Passing Examples
```css
@media (hover: hover) {
.btn:hover {
color: black;
}
}
/* Will traverse nested media queries */
@media (hover: hover) {
@media (min-width: 1px) {
.btn:hover {
color: black;
}
}
}
/* Will traverse nested media queries */
@media (min-width: 1px) {
@media (hover: hover) {
@media (min-width: 100px) {
.btn:hover {
color: black;
}
}
}
}
```
❌ Failing Examples
```css
.fail-btn:hover {
color: black;
}
@media (min-width: 1px) {
.fail-btn:hover {
color: black;
}
}
```
---
### No Fixed Sizes
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/fixed-sizes/)
Fixed pixel (px) values prevent layouts from adapting to different screen sizes, user preferences, and device contexts. When widths, heights, spacing, and breakpoints are defined with px, content can overflow on small screens, create excessive whitespace on large displays, or ignore user font-size preferences set for accessibility.
**Enable this rule to:** Require relative or flexible units (rem, em, %, vw, fr, etc.) for sizing properties and media queries, ensuring layouts adapt gracefully across all contexts.
```json
{
"rules": {
"defensive-css/no-fixed-sizes": true
}
}
```
#### No Fixed Sizes Options
**Configuration:** By default, this rule validates critical sizing properties (width, height, font-size), spacing properties (margin, padding, gap), typography properties (line-height, letter-spacing), and responsive at-rules (@media, @container). Use the `at-rules` and `properties` options to customize which are checked or adjust their severity levels.
```ts
type Severity = 'error' | 'warning';
interface SecondaryOptions {
'at-rules'?: Partial<
Record<
CSSType.AtRules, boolean | [boolean, { severity?: Severity }]
>
>;
'properties'?: Partial<
Record<
keyof CSSType.PropertiesHyphen, boolean | [boolean, { severity?: Severity }]
>
>
"severity"?: Severity
}
```
```json
{
"rules": {
"defensive-css/no-fixed-sizes": [true, {
"at-rules": [{ "@container": false }],
"properties": [{ "transform": true, "scroll-margin": [true, { "severity": "warning" }] }],
"severity": "error"
}],
}
}
```
#### No Fixed Sizes Examples
> [!NOTE]
> This rule does not resolve or validate the values of CSS custom properties. Values like `var(--width)` are treated as flexible since their actual values are not determined. Ensure your custom property definitions use relative units if they're used for sizing.
✅ Passing Examples
```css
/* Sizing with relative units */
.box {
width: 50%;
height: 100vh;
font-size: 1.5rem;
}
/* Spacing with flexible units */
.card {
margin: 2rem auto;
padding: 1em 2em;
gap: 1rem;
}
/* Grid with fractional units */
.grid {
grid-template-columns: repeat(3, 1fr);
}
/* Functions with flexible units */
.responsive {
width: clamp(200px, 50%, 800px);
padding: calc(1rem + 2vw);
}
/* Media queries with relative units */
@media (min-width: 48rem) {
.box {
padding: 2rem;
}
}
/* Zero values are allowed */
.reset {
margin: 0;
padding: 0px;
}
/* Custom properties */
.themed {
width: var(--width);
margin: var(--spacing, 1rem);
}
```
❌ Failing Examples
```css
/* Fixed sizing */
.box {
width: 500px;
height: 300px;
font-size: 16px;
}
/* Fixed spacing */
.card {
margin: 20px;
padding: 10px 15px;
gap: 24px;
}
/* Grid with fixed values */
.grid {
grid-template-columns: 100px 1fr 100px;
}
/* Functions with only px */
.fixed {
width: clamp(200px, 400px, 800px);
padding: calc(10px + 5px);
}
/* Media queries with px */
@media (min-width: 768px) {
.box {
padding: 2rem;
}
}
/* Mixed units still fail if px is present */
.mixed {
margin: 1rem 20px;
line-height: 24px;
}
```
---
### No List Style None
> [!TIP]
> This rule is fixable by passing the `{ fix: true }` option.
In Safari, using `list-style: none` on `
- `, `
- ` elements removes list semantics from the accessibility tree, making the list invisible to VoiceOver users. Using `list-style-type: ""` (empty string) achieves the same visual result while preserving accessibility.
**Exception:** Lists inside `` elements maintain their semantics even with `list-style: none`, so this rule allows that pattern.
**Enable this rule to:** Prevent `list-style: none` on lists outside of navigation, requiring the accessible `list-style-type: ""` approach instead.
```json
{
"rules": {
"defensive-css/no-list-style-none": [true, { "fix": true }]
}
}
```#### No List Style None Examples
✅ Passing Examples
```css
/* Recommended: Preserves semantics */
ul {
list-style-type: "";
}/* Exception: Lists inside nav elements retain semantics */
nav ul {
list-style: none;
}/* Other list-style values are fine */
ul {
list-style: disc;
}
```❌ Failing Examples
```css
ul {
list-style: none;
}.menu ul {
list-style: none;
}ol.items {
list-style: none;
}:not(nav) ul {
list-style: none;
}
```---
### No Mixed Vendor Prefixes
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/grouping-selectors)Grouping vendor-prefixed selectors in a single rule can cause the entire rule to be invalid according to the [W3C selector specification](https://www.w3.org/TR/selectors/#grouping). For example, combining `-webkit-` and `-moz-` placeholder selectors will prevent either from working correctly.
**Enable this rule to:** Require vendor-prefixed selectors to be separated into individual rules, ensuring browser-specific styles apply correctly.
```json
{
"rules": {
"defensive-css/no-mixed-vendor-prefixes": true,
}
}
```#### No Mixed Vendor Prefixes Examples
✅ Passing Examples
```css
input::-webkit-input-placeholder {
color: #222;
}input::-moz-placeholder {
color: #222;
}
```❌ Failing Examples
```css
input::-webkit-input-placeholder,
input::-moz-placeholder {
color: #222;
}
```---
### Require At Layer
CSS cascade layers (`@layer`) provide explicit control over specificity ordering, preventing unexpected style overrides in large codebases or design systems. Without layers, the cascade relies solely on source order and specificity, making it fragile and difficult to manage as styles scale. Scoping component styles to a top-level `@layer` ensures predictable cascade behavior and clearer style boundaries.
**Enable this rule to:** Require all style rules to be wrapped in a top-level `@layer` rule, optionally restricting to a set of supported layer names.
```json
{
"rules": {
"defensive-css/require-at-layer": true,
}
}
```#### Require At Layer Options
**Configuration:** By default, this rule requires all styles to be inside any `@layer`. Use the `supportedLayerNames` option to restrict which layer names are allowed.
```ts
interface SecondaryOptions {
severity?: Severity;
supportedLayerNames?: string[];
}
``````json
{
"rules": {
"defensive-css/require-at-layer": [true, {
"supportedLayerNames": ["ds.components", "ds.utilities"],
"severity": "error"
}],
}
}
```#### Require At Layer Examples
✅ Passing Examples
```css
/* Any layer name (without supportedLayerNames) */
@layer components {
div {
color: red;
}
}/* Supported layer name (with supportedLayerNames: ['ds.components']) */
@layer ds.components {
div {
color: red;
}
}
```❌ Failing Examples
```css
/* Not wrapped in any @layer */
div {
color: red;
}/* Unsupported layer name (with supportedLayerNames: ['ds.components']) */
@layer components {
div {
color: red;
}
}
```---
### Require Background Repeat
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/bg-repeat)Background and mask images repeat by default when the container is larger than the image dimensions. On large screens, this can result in unintended tiling effects that break the design.
**Enable this rule to:** Require an explicit `background-repeat` or `mask-repeat` property whenever `background-image` or `mask-image` is used.
```json
{
"rules": {
"defensive-css/require-background-repeat": true,
}
}
```#### Require Background Repeat Options
**Configuration:** By default, this rule validates both background and mask images. Use the `background-repeat` and `mask-repeat` options to control which properties are checked.
```ts
interface SecondaryOptions {
'background-repeat'?: boolean | [boolean, { severity?: Severity }];
'mask-repeat'?: boolean | [boolean, { severity?: Severity }];
}
``````json
{
"rules": {
"defensive-css/require-background-repeat": [true, {
"background-repeat": [true, { "severity": "error" }],
"mask-repeat": false
}],
}
}
```#### Require Background Repeat Examples
✅ Passing Examples
```css
div {
background: url('some-image.jpg') repeat black top center;
}div {
background: url('some-image.jpg') black top center;
background-repeat: no-repeat;
}div {
mask: url('some-image.jpg') repeat top center;
}div {
mask: url('some-image.jpg') top center;
mask-repeat: no-repeat;
}
```❌ Failing Examples
```css
div {
background: url('some-image.jpg') black top center;
}div {
background-image: url('some-image.jpg');
}div {
mask: url('some-image.jpg') top center;
}div {
mask-image: url('some-image.jpg');
}
```---
### No Unsafe Will-Change
> [!WARNING]
> "`will-change` is intended to be used as a last resort, in order to try to deal with existing performance problems. It should not be used to anticipate performance problems." ~ [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/will-change)The `will-change` CSS property hints to browsers about expected changes to an element, allowing them to optimize rendering ahead of time. However, misuse can cause serious performance issues: applying it to too many properties consumes excessive GPU memory, using it on non-composite properties provides no benefit, and applying it via the universal selector (`*`) forces GPU layers on every element, causing catastrophic performance degradation.
**Enable this rule to:** Prevent common `will-change` anti-patterns that harm performance rather than improve it.
```json
{
"rules": {
"defensive-css/no-unsafe-will-change": true
}
}
```#### No Unsafe Will-Change Options
**Configuration:** By default, this rule allows up to 2 properties and errors on violations. Use the options below to customize validation.
```ts
type Severity = 'error' | 'warning';interface SecondaryOptions {
ignore?: (keyof PropertiesHyphen)[];
maxProperties?: number;
severity?: Severity;
}
``````json
{
"rules": {
"defensive-css/no-unsafe-will-change": [true, {
"maxProperties": 3,
"ignore": ["width"],
"severity": "error"
}],
}
}
```#### No Unsafe Will-Change Examples
✅ Passing Examples
```css
/* Single composite property */
.card:hover {
will-change: transform;
}/* Two composite properties (at default limit) */
.modal {
will-change: transform, opacity;
}/* Composite property in pseudo-class */
.button:focus-visible {
will-change: opacity;
}/* With ignore option for filter */
.element {
will-change: transform, opacity, filter;
/* Passes if maxProperties: 3 and ignore: ['filter'] */
}```
❌ Failing Examples
```css
/* Universal selector - forces GPU layers on all elements */
* {
will-change: transform;
}/* Exceeds default maxProperties limit (3 > 2) */
.element {
will-change: transform, opacity, filter;
}/* Non-composite properties (trigger layout/paint) */
.box {
will-change: width, height;
}.positioned {
will-change: top, left;
}.spaced {
will-change: margin, padding;
}/* Mixed: exceeds limit AND contains non-composite property */
.card {
will-change: transform, opacity, width, height;
}/* Universal selector in descendant */
.container > * {
will-change: opacity;
}
```---
### Require Custom Property Fallback
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/css-variable-fallback)CSS custom properties (variables) can fail silently if undefined, potentially breaking layouts or causing visual issues. Providing fallback values ensures graceful degradation when variables are missing or invalid.
**Enable this rule to:** Require all `var()` functions to include a fallback value (e.g., `var(--color, #000)`).
```json
{
"rules": {
"defensive-css/require-custom-property-fallback": true,
}
}
```#### Require Custom Property Fallback Options
**Configuration:** By default, this rule validates all custom properties. Use the `ignore` option to exclude specific patterns, such as global design tokens or component-scoped variables.
```ts
interface SecondaryOptions {
ignore?: (string | RegExp)[];
}
``````json
{
"rules": {
"defensive-css/require-custom-property-fallback": [true, {
"ignore": ["var\\(--exact-match\\)", /var\(--ds-color-.*\)/],
"severity": "warning"
}],
}
}
```#### Require Custom Property Fallback Examples
✅ Passing Examples
```css
div {
color: var(--color-primary, #000);
}
```❌ Failing Examples
```css
div {
color: var(--color-primary);
}
```---
### Require Dynamic Viewport Height
On mobile browsers, the viewport height can change as the address bar and other UI elements collapse or expand during scrolling. Using static viewport units (`100vh` or `100vb`) can cause content to be cut off or create unexpected layout shifts, particularly on iOS Safari and Chrome mobile.
Dynamic viewport units (`100dvh`, `100dvb`) automatically adjust to the current viewport size, accounting for browser UI changes and providing a more reliable layout on mobile devices.
**Enable this rule to:** Flag usage of `100vh` and `100vb` on height-related properties and automatically fix them to use dynamic viewport units.
```json
{
"rules": {
"defensive-css/require-dynamic-viewport-height": true,
}
}
```#### Require Dynamic Viewport Height Options
> [!TIP]
> This rule is fixable by passing the `{ fix: true }` option.**Configuration:** By default, this rule validates `height`, `block-size`, `max-height`, and `max-block-size` properties. Use the `properties` option to customize which properties are checked and their severity level.
```ts
interface SecondaryOptions {
fix?: boolean;
properties?: {
'block-size'?: boolean | [boolean, SeverityProps];
height?: boolean | [boolean, SeverityProps];
'max-block-size'?: boolean | [boolean, SeverityProps];
'max-height'?: boolean | [boolean, SeverityProps];
'min-block-size'?: boolean | [boolean, SeverityProps];
'min-height'?: boolean | [boolean, SeverityProps];
};
}
``````json
{
"rules": {
"defensive-css/require-dynamic-viewport-height": [true, {
"fix": true,
"properties": {
"height": [true, { "severity": "error" }],
"min-block-size": false,
},
"severity": "warning"
}],
}
}
```#### Require Dynamic Viewport Height Examples
✅ Passing Examples
```css
.hero {
height: 100dvh;
}.container {
block-size: 100dvb;
}.modal {
max-height: 100dvh;
}/* Small and large viewport units are also valid */
.element {
height: 100svh;
max-height: 100lvh;
}/* Non-100 viewport units are allowed */
.partial {
height: 50vh;
max-height: 75vb;
}/* min-height is not validated */
.flexible {
min-height: 100vh;
}/* Width properties are not affected */
.wide {
width: 100vw;
}
```❌ Failing Examples
```css
.hero {
height: 100vh;
}.container {
block-size: 100vb;
}.modal {
max-height: 100vh;
}.overlay {
max-block-size: 100vb;
}/* Also flags usage in functions */
.calculated {
height: calc(100vh - 20px);
}.clamped {
block-size: clamp(100vb, 50vb, 100vb);
}
```---
### Require Flex Wrap
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/flex-wrap)Flex containers do not wrap their children by default. When there isn't enough horizontal space, flex items will overflow rather than wrapping to a new line, potentially breaking layouts on smaller screens.
**Enable this rule to:** Require an explicit `flex-wrap` property (or `flex-flow` shorthand) for all flex containers, ensuring predictable wrapping behavior is defined.
```json
{
"rules": {
"defensive-css/require-flex-wrap": true,
}
}
```#### Require Flex Wrap Examples
✅ Passing Examples
```css
div {
display: flex;
flex-wrap: wrap;
}div {
display: flex;
flex-wrap: nowrap;
}div {
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap-reverse;
}div {
display: flex;
flex-flow: row wrap;
}div {
display: flex;
flex-flow: row-reverse nowrap;
}
```❌ Failing Examples
```css
div {
display: flex;
}div {
display: flex;
flex-direction: row;
}div {
display: flex;
flex-flow: row;
}
```---
### Require Focus Visible
The `:focus` pseudo-class shows focus indicators for both mouse clicks and keyboard navigation, which often leads developers to hide focus outlines entirely (creating accessibility issues). The `:focus-visible` pseudo-class only shows focus indicators when the user is navigating with a keyboard, providing a better user experience.
**Enable this rule to:** Require `:focus-visible` instead of `:focus` for better keyboard navigation UX.
```json
{
"rules": {
"defensive-css/require-focus-visible": true,
}
}
```#### Require Focus Visible Examples
✅ Passing Examples
```css
.btn:focus-visible {
outline: 2px solid blue;
}.modal:focus-within {
border: 1px solid blue;
}/* Intentional exclusion */
.input:not(:focus) {
border: 1px solid gray;
}
```❌ Failing Examples
```css
.btn:focus {
outline: 2px solid blue;
}button:focus {
outline: none;
}.input:focus:hover {
border-color: blue;
}
```---
### Require Named Grid Lines
Unnamed grid lines make layouts harder to understand and maintain. Numeric positions like `grid-column: 1 / 3` are ambiguous and prone to errors when the grid structure changes. Named lines like `[sidebar-start]` provide clarity and self-documenting code.
**Enable this rule to:** Require all grid tracks to be associated with named lines using the `[name]` syntax in `grid-template-columns`, `grid-template-rows`, and the `grid` shorthand.
```json
{
"rules": {
"defensive-css/require-named-grid-lines": true,
}
}
```#### Require Named Grid Lines Options
**Configuration:** By default, this rule validates both row and column lines. Use the `columns` and `rows` options to control which axes are checked.
```ts
interface SecondaryOptions {
columns?: boolean | [boolean, { severity?: Severity }];
rows?: boolean | [boolean, { severity?: Severity }];
}
``````json
{
"rules": {
"defensive-css/require-named-grid-lines": [true, {
"columns": [true, { "severity": "error" }],
"rows": [true, { "severity": "warning" }]
}],
}
}
```#### Require Named Grid Lines Examples
✅ Passing Examples
```css
div {
grid-template-columns: [c-a] 1fr [c-b] 1fr;
}div {
grid-template-rows: [r-a] 1fr [r-b] 2fr;
}div {
grid-template-columns: [a] [b] 1fr [c] 2fr;
}div {
grid-template-columns: repeat(auto-fit, [line-a line-b] 300px);
}div {
grid-template-rows: repeat(auto-fill, [r1 r2] 100px);
}div {
grid: [r-a] 1fr / [c-a] 1fr [c-b] 2fr;
}div {
grid-template-columns: repeat(auto-fit, [a]300px);
}
```❌ Failing Examples
```css
div {
grid-template-columns: 1fr 1fr;
}div {
grid-template-rows: 1fr 1fr;
}div {
grid-template-columns: repeat(3, 1fr);
}div {
grid-template-rows: repeat(3, 1fr);
}div {
grid: auto / 1fr 1fr;
}div {
grid: repeat(3, 1fr) / auto;
}div {
grid-template-columns: 1fr [after] 1fr;
}/* Reserved identifiers cannot be used as line names */
div {
grid-template-columns: [auto] 1fr;
}div {
grid-template-rows: [span] 1fr;
}
```---
### Require Overscroll Behavior
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scroll-chain)Scroll chaining occurs when a scrollable element reaches its scroll boundary and the scroll continues to the parent container. This commonly happens in modals where scrolling past the end causes the background content to scroll, creating a disorienting user experience.
**Enable this rule to:** Require an `overscroll-behavior` property for all scrollable containers (`overflow: auto` or `overflow: scroll`), preventing unintended scroll chaining.
```json
{
"rules": {
"defensive-css/require-overscroll-behavior": true,
}
}
```#### Require Overscroll Behavior Options
**Configuration:** By default, this rule validates both horizontal and vertical overflow. Use the `x` and `y` options to control which axes are checked.
```ts
interface SecondaryOptions {
x?: boolean | [boolean, { severity?: Severity }];
y?: boolean | [boolean, { severity?: Severity }];
}
``````json
{
"rules": {
"defensive-css/require-overscroll-behavior": [true, {
"x": [true, { "severity": "warning" }],
"y": [true, { "severity": "error" }]
}],
}
}
```#### Require Overscroll Behavior Examples
✅ Passing Examples
```css
div {
overflow-x: auto;
overscroll-behavior-x: contain;
}div {
overflow: hidden scroll;
overscroll-behavior: contain;
}div {
overflow: hidden; /* No overscroll-behavior is needed in the case of hidden */
}div {
overflow-block: auto;
overscroll-behavior: none;
}
```❌ Failing Examples
```css
div {
overflow-x: auto;
}div {
overflow: hidden scroll;
}div {
overflow-block: auto;
}
```---
### Require Prefers Reduced Motion
> [!TIP]
> [Read more about prefers-reduced-motion on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)Some users experience motion sickness or vestibular disorders that make animations uncomfortable or even nauseating. The `prefers-reduced-motion` media query allows users to request minimal animation. Respecting this preference is crucial for accessibility.
**Enable this rule to:** Require all animations and transitions to be wrapped in a `@media (prefers-reduced-motion: no-preference)` or `@media not (prefers-reduced-motion: reduce)` query.
```json
{
"rules": {
"defensive-css/require-prefers-reduced-motion": true
}
}
```#### Require Prefers Reduced Motion Examples
✅ Passing Examples
```css
@media (prefers-reduced-motion: no-preference) {
.box {
transition: transform 0.3s;
}
}@media (prefers-reduced-motion: no-preference) {
.box {
animation: slide 1s ease;
}
}/* Instant transitions are allowed */
.box {
transition: transform 0s;
}/* No animation is allowed */
.box {
animation: none;
}@media not (prefers-reduced-motion: reduce) {
.box {
transition: transform 0s;
}
}/* Nested media queries */
@media (prefers-reduced-motion: no-preference) {
@media (min-width: 768px) {
.box {
transition: transform 0.3s;
}
}
}```
❌ Failing Examples
```css
.box {
transition: transform 0.3s;
}.box {
animation: slide 1s ease;
}.box {
animation-duration: 0.5s;
}/* Media query without prefers-reduced-motion */
@media (min-width: 768px) {
.box {
transition: transform 0.3s;
}
}
```---
### Require Scrollbar Gutter
> [!NOTE]
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scrollbar-gutter)When content grows and triggers a scrollbar, the sudden appearance of the scrollbar causes a layout shift as content reflows to accommodate it. This creates a jarring visual jump, especially in dynamic interfaces where content changes frequently.
**Enable this rule to:** Require a `scrollbar-gutter` property for all scrollable containers, reserving space for the scrollbar and preventing layout shifts.
```json
{
"rules": {
"defensive-css/require-scrollbar-gutter": true,
}
}
```#### Require Scrollbar Gutter Options
**Configuration:** By default, this rule validates both horizontal and vertical overflow. Use the `x` and `y` options to control which axes are checked.
```ts
interface SecondaryOptions {
x?: boolean | [boolean, { severity?: Severity }];
y?: boolean | [boolean, { severity?: Severity }];
}
``````json
{
"rules": {
"defensive-css/require-scrollbar-gutter": [true, {
"x": [true, { "severity": "warning" }],
"y": [true, { "severity": "error" }]
}],
}
}
```#### Require Scrollbar Gutter Examples
✅ Passing Examples
```css
div {
overflow-x: auto;
scrollbar-gutter: auto;
}div {
overflow: hidden scroll;
scrollbar-gutter: stable;
}div {
overflow: hidden; /* No scrollbar-gutter is needed in the case of hidden */
}div {
overflow-block: auto;
scrollbar-gutter: stable both-edges;
}
```❌ Failing Examples
```css
div {
overflow-x: auto;
}div {
overflow: hidden scroll;
}div {
overflow-block: auto;
}
```## Troubleshooting
### Third-Party False Positives
If you're getting warnings for properties you don't control (e.g., from third-party libraries), you can disable the rule for specific files in your Stylelint config file using the `overrides` property.
```json
{
"overrides": [
{
"files": ["vendor/**/*.css"],
"rules": {
"defensive-css/no-mixed-vendor-prefixes": null
}
}
]
}
```### Ignoring Specific Patterns
As an escape hatch, use Stylelint's built-in `disable` comments to bypass specific rules:
```css
div {
/* stylelint-disable-next-line defensive-css/require-background-repeat */
background: url(./some-image.jpg);
}
```
- `, or `