{"playbook":"HTML and CSS","url":"https://agentwranglers.se/playbook/html-css","updated":"2026-10-21","license":"https://creativecommons.org/licenses/by/4.0/","credit":"Required by the licence: credit Agent Wranglers with a link to the rule or playbook you used.","how":"Each rule's How text is on its own page: add .md to its url.","rules":[{"chapter":"Structure and files","name":"Load every stylesheet from one entry file","level":"Recommended","rule":"Load all CSS through one entry file that imports every other stylesheet in a fixed order. Pages link that one built file and nothing else.","check":"Every built page has exactly one `<link rel=\"stylesheet\">`, and it points to the bundled entry file. The entry file holds only `@layer` and `@import` statements.","url":"https://agentwranglers.se/playbook/html-css/structure-and-files/one-css-entry-file","related":["/playbook/html-css/structure-and-files/five-style-layers","/playbook/html-css/the-cascade/declare-layer-order-once"],"sources":[]},{"chapter":"Structure and files","name":"Sort styles into five layers","level":"Recommended","rule":"Put every style in one of five layers, in the order tokens, base, layout, components and utilities. Each layer has its own folder, and a file belongs to exactly one layer.","check":"Every CSS file sits in one of the five folders. Files in `tokens/` declare only custom properties. Files in `base/` contain no class selectors. Files in `utilities/` define only classes on the written list.","url":"https://agentwranglers.se/playbook/html-css/structure-and-files/five-style-layers","related":["/playbook/html-css/structure-and-files/one-css-entry-file","/playbook/app-ui/styles-and-components/design-tokens","/playbook/website/site-architecture/module-class-names","/playbook/html-css/the-cascade/declare-layer-order-once"],"sources":[]},{"chapter":"Structure and files","name":"Give each component exactly one stylesheet","level":"Recommended","rule":"Keep all CSS for a component in one file named after it. No other file may select that component's classes.","check":"For every class prefix, a script lists the files whose selectors use it. Each prefix appears in exactly one file, except utilities.","url":"https://agentwranglers.se/playbook/html-css/structure-and-files/component-stylesheet","related":["/playbook/app-ui/styles-and-components/component-variants","/playbook/html-css/the-cascade/important-only-in-utilities","/playbook/html-css/the-cascade/source-order-wins","/playbook/html-css/html-structure-and-reuse/custom-element-names","/playbook/html-css/checks-in-code/unknown-and-duplicate-css"],"sources":[]},{"chapter":"Structure and files","name":"Keep media and container queries with their component","level":"Recommended","rule":"Write every media or container query inside the stylesheet of the component it changes. Never collect them in a separate file for screen sizes or print.","check":"No stylesheet exists whose name contains `responsive`, `mobile`, `print` or `breakpoints`. Inside each component file, every selector within `@media` or `@container` starts with the file's prefix.","url":"https://agentwranglers.se/playbook/html-css/structure-and-files/queries-with-the-component","related":["/playbook/website/interface/container-queries"],"sources":[]},{"chapter":"Structure and files","name":"Write styles only in stylesheets","level":"Recommended","rule":"Keep every style declaration in a stylesheet. Templates have no `style` attributes or `<style>` elements, and scripts never set `element.style` properties.","check":"The built HTML has 0 `style=` attributes and 0 `<style>` elements. A search of the scripts finds 0 assignments to `.style.` other than `setProperty('--…')`.","url":"https://agentwranglers.se/playbook/html-css/structure-and-files/styles-only-in-stylesheets","related":["/playbook/engineering/rules-as-code/build-gates"],"sources":[{"label":"HTML Standard on the style attribute","url":"https://html.spec.whatwg.org/multipage/dom.html#the-style-attribute"},{"label":"CSP Level 3 style-src","url":"https://www.w3.org/TR/CSP3/#directive-style-src"}]},{"chapter":"The cascade","name":"Declare the layer order once","level":"Recommended","rule":"Declare every cascade layer in one `@layer` statement at the top of the entry file. Put every style inside a named layer, so no style is left unlayered.","check":"The first statement of the entry file is one `@layer` list. Every other `@layer` or `layer()` name in the project appears in that list. The built CSS has 0 declarations outside a layer.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/declare-layer-order-once","related":["/playbook/html-css/structure-and-files/one-css-entry-file","/playbook/html-css/structure-and-files/five-style-layers","/playbook/html-css/the-cascade/vendor-css-layer","/playbook/html-css/checks-in-code/check-built-css"],"sources":[{"label":"CSS Cascade 5 on cascade layers","url":"https://www.w3.org/TR/css-cascade-5/#layering"}]},{"chapter":"The cascade","name":"Cap selector specificity","level":"Recommended","rule":"Keep every selector at a specificity of `0,2,0` or lower. That is at most two classes, attributes or pseudo-classes, with no ids.","check":"The linter rule `selector-max-specificity` set to `0,2,0` reports 0 problems on the whole project.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/specificity-limit","related":["/playbook/html-css/checks-in-code/style-linter","/playbook/html-css/naming-and-state/no-position-selectors"],"sources":[{"label":"Selectors Level 4 on specificity","url":"https://www.w3.org/TR/selectors-4/#specificity-rules"}]},{"chapter":"The cascade","name":"Never style by id","level":"Recommended","rule":"Use ids only for links, labels and scripts. No CSS selector contains an id.","check":"The linter rule `selector-max-id` set to `0` reports 0 problems.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/no-id-selectors","related":["/playbook/html-css/html-structure-and-reuse/unique-stable-ids"],"sources":[{"label":"Selectors Level 4 on specificity","url":"https://www.w3.org/TR/selectors-4/#specificity-rules"}]},{"chapter":"The cascade","name":"Keep !important inside the utilities layer","level":"Recommended","rule":"Write `!important` only in the utilities layer. Every other layer has 0 `!important` declarations.","check":"A search of every stylesheet outside the `utilities/` folder finds 0 occurrences of `!important`.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/important-only-in-utilities","related":["/playbook/html-css/structure-and-files/component-stylesheet"],"sources":[{"label":"CSS Cascade 5 on importance","url":"https://www.w3.org/TR/css-cascade-5/#importance"}]},{"chapter":"The cascade","name":"Fence each component's styles in","level":"Recommended","rule":"Make every selector in a component file start at the component's root. Use the module prefix on each class, or wrap the file in `@scope` on the root class.","check":"Every selector in a component file starts with that file's prefix, or the file's only top-level rule is `@scope (.<prefix>)`.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/scope-component-styles","related":["/playbook/website/site-architecture/module-class-names"],"sources":[{"label":"CSS Cascade 6 on scoped styles","url":"https://www.w3.org/TR/css-cascade-6/#scoped-styles"}]},{"chapter":"The cascade","name":"Let source order settle a tie","level":"Avoid","rule":"Never depend on which file loads last to decide between two rules of the same layer and specificity.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/source-order-wins","related":["/playbook/html-css/structure-and-files/component-stylesheet"],"sources":[]},{"chapter":"The cascade","name":"Import third-party CSS into its own layer","level":"Recommended","rule":"Import every third-party stylesheet into a named layer placed before your own layers. Never edit a vendor file in place.","check":"Every `@import` of a file from a package folder carries `layer(vendor)`. The repository holds 0 copies of vendor CSS files, because each one is read from the package folder.","url":"https://agentwranglers.se/playbook/html-css/the-cascade/vendor-css-layer","related":["/playbook/html-css/the-cascade/declare-layer-order-once"],"sources":[{"label":"CSS Cascade 5 on @import","url":"https://www.w3.org/TR/css-cascade-5/#at-import"}]},{"chapter":"Naming and state","name":"Name classes for what the element is","level":"Recommended","rule":"Name each class after the element's job in its module, such as `invoice-total`. Never name it after a look, such as `big-red` or `left-box`.","check":"A script finds 0 class names, outside the utilities list, that contain a word from a fixed list of look words. The list holds colour names, `big`, `small`, `left`, `right`, `bold` and `round`.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/name-by-role","related":["/playbook/website/site-architecture/module-class-names","/playbook/app-ui/styles-and-components/purpose-tokens"],"sources":[]},{"chapter":"Naming and state","name":"Use one naming pattern for every class","level":"Recommended","rule":"Write every class name in lowercase words joined by single hyphens, starting with the module prefix. One regular expression describes every class in the project.","check":"The linter rule `selector-class-pattern` set to the project's pattern reports 0 problems, and a markup check finds 0 `class` values that break it.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/one-class-name-pattern","related":["/playbook/html-css/checks-in-code/style-linter","/playbook/html-css/html-structure-and-reuse/one-root-per-template"],"sources":[]},{"chapter":"Naming and state","name":"Show state through attributes","level":"Recommended","rule":"Express state with the attribute that carries it, such as `aria-expanded`, `aria-current`, `aria-invalid`, `disabled`, `hidden` or `data-state`. Never add a state class such as `is-open` or `active`.","check":"The stylesheets have 0 class selectors starting with `is-` or `has-` or equal to `active`, `open`, `selected` or `current`. Every element with `aria-controls` that opens or closes its target, such as a disclosure or menu button, also has `aria-expanded`.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/state-in-attributes","related":["/playbook/app-ui/styles-and-components/component-catalogue","/playbook/html-css/naming-and-state/separate-script-hooks"],"sources":[{"label":"WCAG 2.2 Name, Role, Value","url":"https://www.w3.org/TR/WCAG22/#name-role-value"},{"label":"WAI-ARIA 1.2 aria-expanded","url":"https://www.w3.org/TR/wai-aria-1.2/#aria-expanded"}]},{"chapter":"Naming and state","name":"Never style by position or deep nesting","level":"Recommended","rule":"Select elements in a component by their own class, never by tag name, position or ancestry. A selector has at most 3 compound parts and 0 type selectors in the components layer.","check":"In the components folder, the linter rules `selector-max-compound-selectors` set to `3` and `selector-max-type` set to `0` report 0 problems. A search finds 0 uses of `:nth-child`, `:nth-of-type`, `:first-child` or `:last-child` there.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/no-position-selectors","related":["/playbook/html-css/the-cascade/specificity-limit","/playbook/html-css/html-structure-and-reuse/no-wrapper-divs"],"sources":[]},{"chapter":"Naming and state","name":"Prefix a component's custom properties with its name","level":"Recommended","rule":"Name every custom property defined in a component file with the module prefix, such as `--order-card-gap`. Set it from a global token, and let variants change only these local properties.","check":"Every custom property declared in a component file starts with `--` followed by that file's prefix. Component files declare 0 global token names.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/component-custom-properties","related":["/playbook/app-ui/styles-and-components/design-tokens","/playbook/website/interface/colour-as-tokens"],"sources":[{"label":"CSS Custom Properties Level 1","url":"https://www.w3.org/TR/css-variables-1/#defining-variables"}]},{"chapter":"Naming and state","name":"Keep script hooks apart from style hooks","level":"Recommended","rule":"Let scripts find elements through `data-` attributes or ids, never through styling classes. Stylesheets never select the attributes that scripts use as hooks.","check":"A search of the scripts finds 0 `querySelector` or `closest` calls with a class selector. A search of the stylesheets finds 0 selectors on `data-action`.","url":"https://agentwranglers.se/playbook/html-css/naming-and-state/separate-script-hooks","related":["/playbook/html-css/naming-and-state/state-in-attributes"],"sources":[]},{"chapter":"HTML structure and reuse","name":"Use the native element first","level":"Required","risk":"law","rule":"Use the HTML element made for the job before a `div` or `span` with a role. Actions are `<button>`, navigation is `<a href>`, lists are `<ul>` or `<ol>`, and page regions are `header`, `nav`, `main` and `footer`.","check":"A markup linter with the rules `prefer-native-element` and `no-redundant-role` reports 0 problems on the built pages. The built HTML has 0 `div` or `span` elements with a click handler or `role=\"button\"`.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/native-elements-first","related":["/playbook/website/html-basics/valid-html","/playbook/html-css/checks-in-code/markup-linter"],"sources":[{"label":"WCAG 2.2 Name, Role, Value","url":"https://www.w3.org/TR/WCAG22/#name-role-value"},{"label":"HTML Standard on the button element","url":"https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element"}]},{"chapter":"HTML structure and reuse","name":"Pass data to templates instead of markup","level":"Recommended","rule":"Give every template or component function plain data, such as text, numbers, lists and named options. Callers never pass HTML strings or extra class names into it.","check":"A unit test renders each template with `<script>` in every text field. The output contains 0 raw `<script>` tags. No template signature accepts a parameter named `html`, `className` or `class`.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/partials-take-data","related":["/playbook/app-ui/styles-and-components/component-variants","/playbook/website/site-architecture/shared-html-modules-only"],"sources":[]},{"chapter":"HTML structure and reuse","name":"Give each template one root element","level":"Recommended","rule":"Make every template return exactly one root element that carries the module's root class. Callers never wrap it in an extra element to change how it looks.","check":"A unit test parses each template's output. It finds exactly 1 top-level element, and that element's class list starts with the module prefix.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/one-root-per-template","related":["/playbook/app-ui/styles-and-components/shared-components","/playbook/html-css/naming-and-state/one-class-name-pattern"],"sources":[]},{"chapter":"HTML structure and reuse","name":"Keep ids unique and stable","level":"Recommended","rule":"Give each id on a page a value that appears once on that page and stays the same in every build. Make it from the content or the data key, never from a counter or a random number.","check":"Every built page has 0 duplicate ids. Two builds of the same content produce the same list of ids on every page.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/unique-stable-ids","related":["/playbook/html-css/the-cascade/no-id-selectors"],"sources":[{"label":"HTML Standard on the id attribute","url":"https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute"}]},{"chapter":"HTML structure and reuse","name":"Avoid wrapper elements that only hold a class","level":"Recommended","rule":"Put the class on the element that already exists instead of adding a `div` around it. A `div` without attributes never wraps another `div` directly.","check":"A build check over every page finds 0 `div` elements without attributes whose first child is a `div`.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/no-wrapper-divs","related":["/playbook/html-css/naming-and-state/no-position-selectors"],"sources":[{"label":"HTML Standard on the div element","url":"https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element"}]},{"chapter":"HTML structure and reuse","name":"Clone markup that scripts add from a template element","level":"Recommended","rule":"Keep every piece of markup that a script inserts in a `<template>` element in the page. Scripts clone it and fill in text, and never build HTML in strings.","check":"A search of the scripts finds 0 string literals that contain an HTML tag and 0 assignments to `innerHTML` or `outerHTML`. Every `<template>` id that a script clones exists in a built page.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/template-element","related":["/playbook/adversarial-testing/injection/cross-site-scripting","/playbook/website/security/trusted-types"],"sources":[{"label":"HTML Standard on the template element","url":"https://html.spec.whatwg.org/multipage/scripting.html#the-template-element"}]},{"chapter":"HTML structure and reuse","name":"Name custom elements with the project prefix","level":"Recommended","rule":"Give every custom element a name that starts with one project prefix and a hyphen, such as `bakery-basket`. Define each name in exactly one file.","check":"Every name passed to `customElements.define` matches `^bakery-[a-z0-9]+(-[a-z0-9]+)*$`, with the project's own prefix. Each name appears in exactly 1 `define` call across the source.","url":"https://agentwranglers.se/playbook/html-css/html-structure-and-reuse/custom-element-names","related":["/playbook/html-css/structure-and-files/component-stylesheet"],"sources":[{"label":"HTML Standard on valid custom element names","url":"https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name"},{"label":"HTML Standard on customElements.define","url":"https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define"}]},{"chapter":"Checks in code","name":"Lint styles with one shared config","level":"Recommended","rule":"Run a style linter such as stylelint over every stylesheet in the build, with one config file in the repository. The build fails on any error or warning.","check":"The repository has exactly 1 style linter config. The build runs the linter with a warning limit of 0, and the build fails when you add `#header { }` to any stylesheet.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/style-linter","related":["/playbook/html-css/the-cascade/specificity-limit","/playbook/html-css/naming-and-state/one-class-name-pattern","/playbook/engineering/rules-as-code/check-every-rule-in-code"],"sources":[]},{"chapter":"Checks in code","name":"Fail on unknown and duplicate CSS","level":"Recommended","rule":"Make the linter reject unknown properties, values, at-rules and type selectors, and duplicate selectors or properties.","check":"The linter rules in the example below are all on. The build fails when you add `dispaly: grid` or a second `.order-card` block to a component file.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/unknown-and-duplicate-css","related":["/playbook/html-css/structure-and-files/component-stylesheet"],"sources":[]},{"chapter":"Checks in code","name":"Remove selectors that match nothing","level":"Recommended","rule":"Fail the build when a class or attribute selector in the stylesheets matches no element in any built page. The only exceptions sit on one list of states and classes that scripts add.","check":"The build reports 0 classes that appear in the stylesheets but in no built page and not on the exception list. Adding `.unused-test { }` to a component file stops the build.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/unused-selectors","related":["/playbook/website/site-architecture/module-class-names"],"sources":[]},{"chapter":"Checks in code","name":"Lint the built markup","level":"Recommended","rule":"Run a markup linter such as html-validate over every built page, with rules for structure on top of validity. Turn on rules against inline styles, duplicate ids, redundant roles and ARIA patterns that have a native element.","check":"The markup linter reports 0 errors on every built page. Adding `<div style=\"margin:0\">` or a second `id=\"menu\"` to a page stops the build.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/markup-linter","related":["/playbook/html-css/html-structure-and-reuse/native-elements-first","/playbook/website/html-basics/valid-html"],"sources":[]},{"chapter":"Checks in code","name":"Check the built CSS as well as the source","level":"Recommended","rule":"Run the specificity, layer and unused-selector checks on the CSS the build produces, not only on the source files.","check":"The build runs the style linter on the output file in `dist/` with the same limits as on the source. It reports 0 problems.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/check-built-css","related":["/playbook/html-css/the-cascade/declare-layer-order-once"],"sources":[]},{"chapter":"Checks in code","name":"Keep every exception in the linter config","level":"Recommended","rule":"Write every exception to a lint rule in the config file, with a reason and a file path. Source files contain 0 comments that turn a linter off.","check":"A search of the stylesheets and templates finds 0 `stylelint-disable` or `html-validate-disable` comments. Every override in the config names a file path and has a comment with its reason on the line above.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/one-exceptions-list","related":["/playbook/engineering/rules-as-code/check-every-rule-in-code"],"sources":[]},{"chapter":"Checks in code","name":"Lint every feature against one Baseline target","level":"Recommended","rule":"Choose one Baseline target for the project, such as \"widely available\" or a year. Fail the build on any CSS or HTML feature outside it that no `@supports` test guards.","check":"The CSS and HTML linters both run a Baseline rule with the same target and report 0 problems. Adding an unguarded feature newer than the target to a stylesheet stops the build.","url":"https://agentwranglers.se/playbook/html-css/checks-in-code/baseline-target","related":["/playbook/website/interface/scroll-driven-animations","/playbook/website/site-architecture/progressive-enhancement"],"sources":[{"label":"CSS Conditional Rules Level 3 on @supports","url":"https://www.w3.org/TR/css-conditional-3/#at-supports"},{"label":"CSS Snapshot 2026","url":"https://www.w3.org/TR/css-2026/"}]}]}