Stylelint Plugin Defensive Css
A Stylelint plugin to help you write more defensive, accessible, and maintainable CSS
Install / Use
npx skills add yuschick/stylelint-plugin-defensive-cssInstalls into whichever agent you are using.
README

A Stylelint plugin to help you write more defensive, accessible, and maintainable CSS. Catch layout and accessibility bugs before they ship, enforce team-wide best practices, and guard against the subtle CSS pitfalls that break real-world experiences.
Table of Contents
Getting Started | Quickstart | Plugin Configs | Plugin Rules | Troubleshooting
Getting Started
[!IMPORTANT] The plugin requires Stylelint v14.0.0 or greater.
To get started using the plugin, it must first be installed.
npm i stylelint-plugin-defensive-css --save-dev
yarn add stylelint-plugin-defensive-css --dev
With the plugin installed, it must be added to the plugins array of your Stylelint config.
{
"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:
{
"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:
{
"extends": ["stylelint-plugin-defensive-css/configs/recommended"]
}
Equivalent to:
{
"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-clamp-font-size": [
true,
{ "reportUnresolvable": [true, { "severity": "warning" }], "severity": "error" }
],
"defensive-css/no-unsafe-will-change": [true, { "severity": "error" }],
"defensive-css/no-user-select-none": [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-forced-colors-focus": [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" }],
"defensive-css/require-pure-selectors": [
true,
{ "ignoreElements": ["*"], "severity": "error" }
],
"defensive-css/require-system-font-fallback": [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:
{
"extends": ["stylelint-plugin-defensive-css/configs/accessibility"]
}
Equivalent to:
{
"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-unsafe-clamp-font-size": [
true,
{ "reportUnresolvable": [true, { "severity": "warning" }], "severity": "error" }
],
"defensive-css/no-user-select-none": [true, { "severity": "error" }],
"defensive-css/require-focus-visible": [true, { "severity": "error" }],
"defensive-css/require-forced-colors-focus": [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:
{
"extends": ["stylelint-plugin-defensive-css/configs/strict"]
}
Defensive CSS Rules
The plugin provides multiple rules that can be toggled on and off as needed.
- No Accidental Hover
- No Fixed Sizes
- No List Style None
- No Mixed Vendor Prefixes
- No Unsafe Clamp Font Size
- No Unsafe Will-Change
- No User Select None
- Require At Layer
- Require Background Repeat
- Require Custom Property Fallback
- Require Dynamic Viewport Height
- Require Flex Wrap
- Require Focus Visible
- Require Forced Colors Focus
- Require Grid Minmax
- Require Named Grid Lines
- Require Overscroll Behavior
- Require Prefers Reduced Motion
- Require Pure Selectors
- Require Scrollbar Gutter
- Require System Font Fallback
No Accidental Hover
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.
{
"rules": {
"defensive-css/no-accidental-hover": true
}
}
No Accidental Hover Examples
<details> <summary>✅ Passing Examples</summary>@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;
}
}
}
}
</details>
<details>
<summary>❌ Failing Examples</summary>
.fail-btn:hover {
color: black;
}
@media (min-width: 1px) {
.fail-btn:hover {
color: black;
}
}
</details>
No 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.
{
"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.
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;
}
{
"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
<details> <summary>✅ Passing Examples</summary>[!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.
/* Sizing with relative units */
.box {
width: 50%;
height: 100vh;
font-size: 1.5rem;
}
/* Spacing with flexible units */
.card {
margin: 2rem auto;
padding: 1em
Related Skills
node-connect
385.5kDiagnose OpenClaw Android, iOS, or macOS node pairing, QR/setup code, route, auth, and connection failures.
blender-python-addon
40.5kBlender Python add-on rules for operators, panels, properties, registration, testing, and API-safe scripting
flutter-development-guidelines-cursorrules-prompt-file
40.5kCursor rules for Flutter development with MVVM architecture, Riverpod state management, Material widgets, and Dart style guidelines.
commit-push-pr
140.6kCommit, push, and open a PR
