|$ curl https://forge-ai.dev/api/markdown?path=docs/css/best-practices
$cat docs/css-—-best-practices-&-patterns.md
updated Recently·18 min read·published

CSS — Best Practices & Patterns

CSSAdvanced🎯Free Tools
Introduction

CSS is deceptively simple: anyone can change a color or align a box. But as a project grows, the global namespace, cascading rules, and loose coupling between markup and style make CSS one of the hardest languages to keep maintainable. This guide collects field-tested practices for writing CSS that scales from a single landing page to a multi-team design system.

The recommendations here span architecture (how files are organized), naming (how classes are coined), specificity (how conflicts are resolved), responsive behavior, modern features, performance, accessibility, and tooling. Use them as a baseline, and adapt the depth to your team size and product lifecycle.

info

Treat CSS as a real programming language. Use linting, version control, code review, and automated testing just as you would for JavaScript or Python. A stylesheet with hundreds of lines deserves the same rigor as any other module.
Dos and Don'ts

Most CSS pain comes from a small set of recurring mistakes. The table below maps common anti-patterns to recommended alternatives. Internalize the left column as a warning, and the right column as the default choice.

AvoidPreferWhy
#header.site-headerIDs have high specificity and are hard to override
!importantLower specificity or cascade layersImportant escalates conflicts and breaks component reuse
div.container > div.container__itemDeep selectors are brittle and slow to match
px units everywhererem, em, %, ch, viewport unitsRelative units support zoom, theming, and fluid layouts
float-based layoutsFlexbox or GridModern layouts are simpler, robust, and responsive
@import in CSS<link rel="stylesheet"> or bundler imports@import blocks rendering and adds sequential requests
Magic numbersCSS custom propertiesTokens document intent and enable theming
Global reset everythingTargeted resets and normalize layersAggressive resets fight browser defaults unexpectedly
dos-and-donts.css
CSS
1/* Bad — high specificity, hard to reuse */
2#sidebar .widget ul li a {
3 color: #3B82F6;
4}
5
6/* Better — flat, component-scoped */
7.widget__link {
8 color: var(--color-link);
9}
10
11/* Bad — magic numbers */
12.hero {
13 margin-top: 73px;
14}
15
16/* Better — derive from a token */
17.hero {
18 margin-top: var(--header-height);
19}

warning

Avoid using !important in component CSS. It should be reserved for utility classes, temporary debugging, or overriding third-party widgets where you cannot change the source. Once introduced, it tends to spread like a virus through the codebase.
Code Style & Conventions

Consistent code style reduces cognitive load during review and makes diffs cleaner. Adopt a written style guide, enforce it with a linter, and let the formatter handle the mechanical details so humans can focus on architecture.

RuleStandardRationale
Indentation2 spacesFits nested media queries and modern nesting
QuotesDouble for strings and attribute selectorsConsistency with HTML and JSON conventions
Trailing semicolonsRequired on every declarationAvoids silent errors when reordering lines
Leading zeroOmit before decimal point.5rem reads cleaner than 0.5rem
ShorthandUse only when you mean all valuesbackground shorthand resets inherited sub-properties
Color formathex for static, oklch/hsl for themingPerceptually uniform spaces animate better
code-style.css
CSS
1/* Good — consistent, readable, tokenized */
2.card {
3 --card-padding: 1rem;
4 display: flex;
5 flex-direction: column;
6 gap: .5rem;
7 padding: var(--card-padding);
8 background-color: var(--color-surface);
9 border: 1px solid var(--color-border);
10 border-radius: var(--radius-md);
11}
12
13.card__title {
14 margin: 0;
15 font-size: var(--text-lg);
16 line-height: 1.25;
17 color: var(--color-text-strong);
18}
19
20/* Bad — inconsistent, fragile, hard to scan */
21.card{ display:flex;flex-direction:column;gap:8px; padding:16px;
22background:#fff; border:1px solid #ddd; border-radius:8px;}
23.card h2{margin:0; font-size:20px; color:#000}

best practice

Order declarations logically: positioning, box model, typography, visual styles, interactions, animations. Alphabetical ordering is also defensible if your team prefers it, but never leave the order random. Consistency matters more than the exact rule.
Naming Conventions

Class names are the primary interface of your stylesheet. A good name explains what a thing is, not what it looks like. Names like .red-box age poorly when the design system changes; names like .alert--danger survive rebrands.

BEM (Block Element Modifier) is the most battle-tested naming convention for component CSS. Utility-first frameworks such as Tailwind take the opposite approach and encode style directly in class names. Both are valid; mixing them carelessly is where problems arise.

naming-conventions.css
CSS
1/* BEM — flat, predictable, component-scoped */
2.button { }
3.button__icon { }
4.button--primary { }
5.button--large { }
6
7/* Utility-first — composable, terse, design-system aligned */
8.flex { display: flex; }
9.items-center { align-items: center; }
10.gap-4 { gap: 1rem; }
11.text-blue-600 { color: #2563EB; }
12
13/* Hybrid — component shell + utility interior */
14/* In HTML: <article class="card flex flex-col gap-2"> */
15.card {
16 --card-bg: var(--color-surface);
17 background-color: var(--card-bg);
18 border-radius: var(--radius-md);
19}
ConventionBest forTrade-off
BEMHand-authored component CSSVerbose names, zero specificity wars
Utility-firstRapid UI development, design systemsHTML noise, requires build tooling
SMACSSCategorizing rules by functionLess prescriptive, more interpretation
ITCSSLarge codebases, strict specificity triangleDisciplined layering, steeper learning curve
Specificity & Cascade

Specificity determines which rule wins when selectors conflict. The cascade (origin, importance, specificity, source order) is the actual decision path. Most bugs happen when developers fight specificity instead of working with it.

specificity.css
CSS
1/* Specificity from low to high */
2/* 0-0-1 type selector */
3p { color: red; }
4
5/* 0-1-0 class selector */
6.text { color: blue; }
7
8/* 1-0-0 ID selector */
9#lead { color: green; }
10
11/* 0-2-0 two classes beat one */
12.card .text { color: purple; }
13
14/* Inline style beats ID */
15/* <p style="color: orange;"> ... */
16
17/* !important beats everything */
18.text { color: pink !important; }

Modern CSS gives you better tools than ever to manage the cascade. @layer lets you define explicit precedence groups regardless of source order. :where() removes specificity from complex selectors. Use both before reaching for !important.

cascade-layers.css
CSS
1/* Cascade layers — reset before base before components */
2@layer reset, base, components, utilities;
3
4@layer reset {
5 *, *::before, *::after {
6 box-sizing: border-box;
7 }
8}
9
10@layer base {
11 body {
12 font-family: system-ui, sans-serif;
13 line-height: 1.5;
14 }
15}
16
17@layer components {
18 .button {
19 padding: .5rem 1rem;
20 border-radius: var(--radius-md);
21 }
22}
23
24@layer utilities {
25 .visually-hidden {
26 position: absolute;
27 width: 1px;
28 height: 1px;
29 overflow: hidden;
30 }
31}
32
33/* :where() removes specificity */
34:where(.card, .panel) :where(h2, h3) {
35 margin-block-start: 0;
36}
37/* Effective specificity of the selector above is 0-0-1 */

info

Keep most selectors at 0-1-0 specificity (one class). When you need to override, add another class or move to a higher layer rather than using an ID or !important.
Project Structure

A clear file structure separates concerns, reduces merge conflicts, and makes dead code easier to spot. The exact shape depends on your stack — plain CSS, Sass, CSS Modules, Tailwind, or a monorepo design system — but the principles remain the same.

project-structure.txt
TEXT
1src/
2 styles/
3 1-settings/ # Tokens: colors, spacing, typography, breakpoints
4 _colors.css
5 _spacing.css
6 _tokens.css
7 2-tools/ # Mixins, functions (Sass/PostCSS)
8 _mixins.css
9 3-generic/ # Reset / normalize
10 _reset.css
11 4-elements/ # Bare HTML element styles
12 _typography.css
13 5-objects/ # OOCSS layout objects
14 _container.css
15 _grid.css
16 6-components/ # Buttons, cards, modals, etc.
17 _button.css
18 _card.css
19 7-utilities/ # Overrides and helpers
20 _visually-hidden.css
21 _spacing-utilities.css
22 main.css # @layer + @import all partials
23 components/
24 Button/
25 Button.tsx
26 Button.module.css

The numbered folders above follow ITCSS (Inverted Triangle CSS). Lower layers are more generic and lower specificity; higher layers are more specific. This ordering prevents lower-level rules from accidentally overriding component styles.

main.css
CSS
1/* main.css — import in specificity order */
2@layer reset, base, objects, components, utilities;
3
4@import url("1-settings/_tokens.css") layer(base);
5@import url("3-generic/_reset.css") layer(reset);
6@import url("4-elements/_typography.css") layer(base);
7@import url("5-objects/_container.css") layer(objects);
8@import url("6-components/_button.css") layer(components);
9@import url("6-components/_card.css") layer(components);
10@import url("7-utilities/_spacing-utilities.css") layer(utilities);

best practice

Co-locate styles with components when using React, Vue, or Svelte. CSS Modules, scoped styles, or Tailwind keep a component's styles discoverable and deletable alongside its markup. Reserve the global stylesheet for tokens, resets, and cross-cutting utilities.
Responsive Design

Build mobile-first: the base styles target small screens, and media queries add complexity as the viewport grows. This approach produces leaner CSS, clearer overrides, and better performance on constrained devices.

responsive.css
CSS
1/* Mobile-first grid */
2.gallery {
3 display: grid;
4 gap: 1rem;
5 grid-template-columns: 1fr;
6}
7
8@media (min-width: 640px) {
9 .gallery {
10 grid-template-columns: repeat(2, 1fr);
11 }
12}
13
14@media (min-width: 1024px) {
15 .gallery {
16 grid-template-columns: repeat(4, 1fr);
17 }
18}
19
20/* Fluid typography with clamp() */
21.hero__title {
22 font-size: clamp(1.75rem, 4vw + 1rem, 3.5rem);
23}
24
25/* Container queries — responsive to component width, not viewport */
26.card {
27 container-type: inline-size;
28}
29
30@container (min-width: 400px) {
31 .card {
32 display: grid;
33 grid-template-columns: 120px 1fr;
34 }
35}

info

Use logical properties (margin-inline-start, padding-block) instead of directional ones when supporting right-to-left or vertical writing modes. Pair them with container queries for truly component-driven responsive behavior.
Modern Features

CSS is evolving rapidly. Features that once required preprocessors or JavaScript are now native: custom properties, nesting, container queries, subgrid, :has(), color-mix, anchor positioning, and scroll-driven animations. Learn them, but ship them with appropriate fallbacks.

modern-features.css
CSS
1/* CSS custom properties with fallback */
2.button {
3 background-color: var(--button-bg, #2563EB);
4 color: var(--button-text, #FFFFFF);
5}
6
7/* Native nesting */
8.card {
9 padding: 1rem;
10
11 &:hover {
12 box-shadow: 0 4px 6px rgb(0 0 0 / 10%);
13 }
14
15 & .card__title {
16 font-size: var(--text-lg);
17 }
18}
19
20/* :has() — parent and relational selection */
21.card:has(.card__image) {
22 grid-template-columns: 120px 1fr;
23}
24
25.form-group:has(input:invalid) .error {
26 display: block;
27}
28
29/* color-mix for derived colors */
30.alert--info {
31 background-color: color-mix(in oklch, var(--color-info) 15%, transparent);
32 border-left: 4px solid var(--color-info);
33}

When using cutting-edge features, provide fallbacks for older browsers. Use @supports to detect support and layer graceful degradation.

fallbacks.css
CSS
1/* Progressive enhancement for anchor positioning */
2.tooltip {
3 position: absolute;
4}
5
6@supports (position-area: bottom) {
7 .tooltip {
8 position: absolute;
9 position-anchor: --trigger;
10 position-area: bottom;
11 margin-top: .5rem;
12 }
13}
14
15/* Fallback for container queries */
16@container not (min-width: 1px) {
17 /* Styles for browsers without CQ support */
18 .card {
19 display: flex;
20 flex-direction: column;
21 }
22}
Theming

A theme is a layer of tokens mapped to semantic intent. Define primitive tokens (raw colors, spacing values) and semantic tokens (background, text, border) separately. This separation lets you swap themes without rewriting component CSS.

theming.css
CSS
1/* Primitive tokens */
2:root {
3 --blue-500: #3B82F6;
4 --blue-600: #2563EB;
5 --gray-100: #F3F4F6;
6 --gray-900: #111827;
7 --space-1: .25rem;
8 --space-4: 1rem;
9}
10
11/* Semantic tokens */
12:root {
13 --color-bg: #FFFFFF;
14 --color-surface: var(--gray-100);
15 --color-text: var(--gray-900);
16 --color-link: var(--blue-600);
17 --radius-md: .5rem;
18}
19
20/* Dark theme */
21@media (prefers-color-scheme: dark) {
22 :root {
23 --color-bg: var(--gray-900);
24 --color-surface: #1F2937;
25 --color-text: var(--gray-100);
26 --color-link: var(--blue-500);
27 }
28}
29
30/* Manual theme class */
31[data-theme="dark"] {
32 --color-bg: var(--gray-900);
33 --color-surface: #1F2937;
34 --color-text: var(--gray-100);
35 --color-link: var(--blue-500);
36}
37
38body {
39 background-color: var(--color-bg);
40 color: var(--color-text);
41}

best practice

Avoid color-scheme-only theming for production apps. Pair it with explicit custom properties so you control every value and can support multiple branded themes beyond just light and dark.
Performance

CSS performance is usually not the bottleneck, but poor practices can compound: unused rules, expensive selectors, render-blocking sheets, and layout thrashing. Measure before optimizing, then fix the real problems.

performance.css
CSS
1/* Avoid deeply nested selectors — expensive to match */
2/* Bad */
3.sidebar nav ul li a span.icon { }
4
5/* Better */
6.nav__icon { }
7
8/* Contain paint/layout for animated regions */
9.carousel {
10 contain: layout paint;
11}
12
13/* Promote animated elements to their own layer */
14.animated-badge {
15 will-change: transform;
16}
17
18/* Remove will-change after animation completes in JS */
19
20/* Prefer transform and opacity for animations */
21.modal--open {
22 opacity: 1;
23 transform: translateY(0);
24 transition: opacity 200ms ease, transform 200ms ease;
25}
26
27.modal {
28 opacity: 0;
29 transform: translateY(-1rem);
30}

For large applications, split CSS by route or component and load it lazily. Inline critical above-the-fold CSS so the first paint is not blocked by a network request. Tools like Lighthouse, Chrome DevTools Coverage, and PurgeCSS help identify waste.

warning

Do not overuse will-change. It consumes GPU memory and can hurt performance on low-end devices. Add it just before an animation starts and remove it afterward.
Security

CSS is not typically an attack vector on its own, but it can leak information, enable clickjacking, or be injected through untrusted user content. Treat user-supplied styles as untrusted input.

RiskMitigation
User CSS injectionSanitize or sandbox user-provided styles; never render raw CSS
Attribute selectors leaking dataAvoid input[value^="secret"] patterns on sensitive fields
Clickjacking overlaysUse X-Frame-Options / CSP frame-ancestors; review z-index stacking
Third-party widget stylesScope vendor CSS inside a class or Shadow DOM
security.css
CSS
1/* Scope untrusted content styles */
2.user-content {
3 /* whitelist safe properties */
4 line-height: 1.6;
5 color: var(--color-text);
6}
7
8.user-content img {
9 max-width: 100%;
10 height: auto;
11}
12
13/* Avoid attribute selectors on sensitive inputs */
14/* Bad: can leak typed values to a background request */
15input[name="password"][value^="a"] {
16 background-image: url("/leak?char=a");
17}
18
19/* Instead, rely on the browser's built-in password masking */
Testing

CSS testing ranges from automated linting to visual regression. At minimum, enforce style consistency with a linter and catch broken builds with a type checker when using CSS Modules or typed design tokens.

package.json
JSON
1{
2 "scripts": {
3 "lint:css": "stylelint "src/**/*.css"",
4 "format:css": "prettier --write "src/**/*.css"",
5 "test:visual": "playwright test visual"
6 },
7 "devDependencies": {
8 "stylelint": "^16.0.0",
9 "stylelint-config-standard": "^36.0.0",
10 "prettier": "^3.0.0"
11 }
12}

Visual regression testing is the most reliable way to catch unintended CSS changes. Tools like Playwright, Chromatic, or Percy capture screenshots of components and flag pixel differences. Run them in CI before merging layout or styling pull requests.

info

Use BackstopJS or Playwright for local visual regression, and Chromatic for component-level review in Storybook. Keep baseline screenshots under version control or a shared artifact store so CI is deterministic.
Accessibility

CSS is a powerful accessibility tool. It controls focus indication, color contrast, motion preferences, reading order, and target sizes. Accessibility should not be an afterthought — build it into the design system from the first component.

accessibility.css
CSS
1/* Visible, high-contrast focus styles */
2:focus-visible {
3 outline: 3px solid var(--color-focus);
4 outline-offset: 2px;
5}
6
7/* Respect reduced motion preferences */
8@media (prefers-reduced-motion: reduce) {
9 *, *::before, *::after {
10 animation-duration: .01ms !important;
11 animation-iteration-count: 1 !important;
12 transition-duration: .01ms !important;
13 }
14}
15
16/* Minimum touch target size */
17.button {
18 min-height: 44px;
19 min-width: 44px;
20}
21
22/* Do not remove focus outlines without replacement */
23/* Bad */
24button:focus { outline: none; }
25
26/* Better */
27button:focus-visible {
28 outline: 2px dashed var(--color-focus);
29}

best practice

Test color contrast with automated tools (axe, Lighthouse) and manual checks. Do not rely on color alone to convey state — pair it with icons, text labels, or patterns.
Tooling

Modern CSS workflows rely on a toolchain for preprocessing, bundling, linting, formatting, and optimization. Choose tools that match your stack, lock their versions, and configure them in code so the entire team shares the same rules.

stylelint.config.js
JavaScript
1// stylelint.config.js
2export default {
3 extends: ["stylelint-config-standard"],
4 rules: {
5 "selector-class-pattern": [
6 "^([a-z][a-z0-9]*)(_[a-z0-9]+)*$|^([a-z][a-z0-9]*)(__[a-z0-9]+)*(--[a-z0-9]+)?$",
7 {
8 message: "Expected class name to follow BEM or kebab-case",
9 },
10 ],
11 "declaration-property-value-no-unknown": true,
12 "color-no-invalid-hex": true,
13 "unit-disallowed-list": [["px"], { ignoreProperties: { px: ["border-width"] } }],
14 },
15};

PostCSS is the standard CSS transformation pipeline. Plugins can add future-syntax support, autoprefix, purge unused styles, inline imports, and optimize the final bundle. Tailwind CSS is implemented as a PostCSS plugin.

postcss.config.js
JavaScript
1// postcss.config.js
2export default {
3 plugins: {
4 "postcss-import": {},
5 "tailwindcss": {},
6 "autoprefixer": {},
7 "cssnano": process.env.NODE_ENV === "production" ? {} : false,
8 },
9};

info

Run lint and format checks in a pre-commit hook and in CI. Fast feedback loops prevent style debates in pull requests and keep the main branch green.
Code Review Checklist

Use this checklist for every stylesheet or styling pull request. Automate the checks that can be automated, and reserve human review for architecture, semantics, and accessibility.

CheckAutomated?Details
FormattingPrettierConsistent indentation, quotes, semicolons
LintingStylelintNo invalid values, naming convention followed
SpecificityStylelint / reviewNo IDs or !important in component CSS
UnitsStylelintrem/em for typography, tokens for spacing
Contrastaxe / LighthouseWCAG AA minimum for normal text
MotionReviewprefers-reduced-motion respected
FocusReviewVisible focus indicator on interactive elements
ResponsiveReviewMobile-first media queries, fluid sizing
Dead codePurgeCSS / reviewNo unused selectors or commented-out rules
Bundle sizeCI / LighthouseNo unexpected growth, CSS inlined if critical
ci_checklist.sh
Bash
1# Run the full CSS CI pipeline locally
2npm run lint:css
3npm run format:css -- --check
4npx tsc --noEmit
5npm run test:visual
$Blueprint — Engineering Documentation·Section ID: CSS-BP·Revision: 1.0