# The Sass Way — full text > The Sass Way is a tutorial-first publication for front-end developers learning CSS, Sass/SCSS, and modern layout. We write answer-first guides that get to the point, show real code, and explain the why behind the rules. # The Complete Guide to Sass/SCSS: From Setup to Scalable Stylesheets > Sass compiles SCSS into browser-ready CSS and adds build-time variables, modules, mixins, functions, maps, and loops. For a Node project, install the current `sass` package as a development dependency, invoke it through an npm script or `npx sass`, and organize new code with `@use` and `@forward`. Avoid new Sass `@import` rules, global built-in functions, legacy color helpers such as `darken()`, and slash division; they are deprecated in current Dart Sass. Use CSS custom properties for values that must change in the browser. Source: https://thesassway.com/complete-guide-to-sass/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-05 ## Sass in one sentence Sass is a stylesheet language that compiles to CSS. In **SCSS** syntax, you can write ordinary CSS plus build-time variables, modules, mixins, functions, maps, loops, and nesting; the browser receives the generated CSS, not your `.scss` files. This tutorial uses the current **Dart Sass 1.x** workflow and the module system: `@use` and `@forward`. As of August 30, 2026, the official documentation lists **Dart Sass 1.103.1**. Verify your installed version with the CLI and let the project lockfile record its actual dependency. If you want a gentler definition first, read [What is Sass?](/what-is-sass/). If `.sass` and `.scss` sound like two sequels released in the wrong order, our [Sass versus SCSS guide](/sass-vs-scss/) explains the syntax choice. ## Install a project-local compiler For a Node project, install the official `sass` package as a development dependency: ```bash npm install --save-dev sass ``` Then put the commands in `package.json` so the project uses its local dependency consistently: ```json { "scripts": { "css:build": "sass scss/main.scss public/main.css --style=compressed", "css:dev": "sass --watch scss/main.scss public/main.css" } } ``` Run them with: ```bash npm run css:dev npm run css:build npx sass --version ``` An npm script automatically exposes local package executables. Outside a script, `npx sass` makes the local choice explicit. Installing `sass` locally and then assuming a bare global `sass` command is the same version is how “works on my machine” acquires a stylesheet department. Use Dart Sass for new work. Ruby Sass reached end-of-life in 2019, LibSass is deprecated, and Node Sass reached end-of-life in 2024; do not start a new project with `node-sass`. The Sass team also maintains `sass-embedded`, which wraps the Dart VM and can be faster, and publishes standalone executables. Your framework may already integrate Sass. Check its current documentation before adding a parallel compiler that writes the same file from a second direction. The CLI supports one-file mode: ```bash npx sass scss/main.scss public/main.css ``` and directory mapping: ```bash npx sass scss:public/css ``` Useful flags include `--watch`, `--update` (compile only stylesheets whose dependencies are newer than their CSS), `--style=compressed`, `--no-source-map`, and `--load-path`. Add load paths narrowly; a huge search path makes module origins harder to reason about. The fuller setup guide is [how to install and compile Sass](/how-to-install-sass/). ## Your first SCSS file Create `scss/main.scss`: ```scss $brand: #5b3fd6; $space: 1rem; .notice { padding: $space $space * 1.5; border-inline-start: 0.25rem solid $brand; &:hover { background: #f5f2ff; } } ``` Sass replaces the variables, performs the multiplication, expands the nesting, and emits CSS. `$brand` and `$space` do not exist in the browser. That is the first important boundary: | Need | Prefer | |---|---| | Fixed build-time token or math | Sass variable `$space` | | Value that changes by element or theme | CSS custom property `--color` | | Generate repeated static rules | Sass loop or mixin | | Let the cascade choose at runtime | Native CSS | Sass variables are imperative and compiled away, so their names add no bytes to output CSS. They support build-time arithmetic—use normal `+` and `*`, and `math.div()` for division. CSS custom properties stay in the output and add bytes, but inherit and vary at runtime; runtime arithmetic belongs in `calc()`. Modern projects often use both: ```scss $brand: #5b3fd6; :root { --brand: #{$brand}; } .button { background: var(--brand); } ``` The `#{}` syntax is interpolation: it inserts a Sass value where Sass would otherwise pass custom-property text through unchanged. For a broader runtime comparison, see [CSS custom properties](/css-variables-guide/). ## Nest for relationships, not to photocopy the DOM SCSS nesting is helpful for states, pseudo-elements, and tightly related descendants: ```scss .card { padding: 1rem; &:hover { box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 12%); } &__title { margin-block: 0 0.5rem; } } ``` The parent selector `&` compiles `&:hover` to `.card:hover` and `&__title` to `.card__title`. That second form is especially handy with BEM-style names: the source stays grouped while the output remains an ordinary class selector. The same component can still use familiar values such as an `8px` radius, a `1.25rem` title, a `4px 12px` shadow with `rgba(0, 0, 0, 0.1)`, or a `2px` featured border. Sass does not make those design choices better; it simply keeps you from hunting them through a shrubbery of repeated selectors. Do not nest merely because HTML elements sit inside one another. A four-level SCSS doll produces a four-level CSS selector with extra specificity and structural coupling. Keep selectors understandable in the generated CSS. Native CSS nesting now overlaps with this Sass feature, but syntax and edge cases are not identical; [native CSS nesting versus Sass nesting](/css-nesting-vs-sass-nesting/) covers the migration question. ## Modules: use @use, expose with @forward New Sass should use the module system. `@use` loads a stylesheet as a module, includes its CSS once, and gives public variables, mixins, and functions a namespace. ```scss // scss/tokens/_colors.scss $brand: #5b3fd6; $ink: #201a2b; ``` ```scss // scss/components/_button.scss @use "../tokens/colors"; .button { color: white; background: colors.$brand; } ``` ```scss // scss/main.scss @use "components/button"; ``` You may shorten or remove a namespace: ```scss @use "tokens/colors" as c; // c.$brand @use "tokens/spacing" as *; // $space-sm; use sparingly ``` `as *` can cause collisions, so reserve it for modules you control. A file beginning with `_` is a partial and is not emitted as a separate CSS file when a directory is compiled. You omit the underscore and extension in the module URL. `@use` rules must appear before style rules, although configurable variable declarations can precede them. Members loaded in one file do not magically appear in another; each file declares what it uses. That explicitness feels fussy for nine minutes and helpful for the following nine years. Use `@forward` to create one public entrypoint: ```scss // scss/tokens/_index.scss @forward "colors"; @forward "spacing"; ``` ```scss // scss/components/_button.scss // _spacing.scss defines $space-sm and $space-md. @use "../tokens"; .button { padding: tokens.$space-sm tokens.$space-md; background: tokens.$brand; } ``` `@forward` exposes members to downstream users; it does not automatically make them available inside the forwarding file. If a file both forwards and uses the same module, the official docs recommend placing `@forward` first so downstream configuration is applied before the local `@use`. ## Configure a module with !default and with Libraries can publish overridable defaults: ```scss // scss/theme/_index.scss $radius: 0.5rem !default; $brand: #5b3fd6 !default; .theme-button { border-radius: $radius; background: $brand; } ``` Configure the module the first time it is loaded: ```scss // scss/main.scss @use "theme" with ( $radius: 999px, $brand: #006b5f ); ``` Configuration is a module API, so expose only variables you intend consumers to set. Since Dart Sass 1.92.0, configuring a private variable emits a deprecation warning. A leading `-` or `_` marks a member private; do not build a package that relies on reaching through that boundary with a `with` block. ## Mixins output styles; functions return values A mixin packages declarations or rules: ```scss @mixin focus-ring($color) { outline: 0.2rem solid $color; outline-offset: 0.2rem; } .button:focus-visible { @include focus-ring(currentColor); } ``` A function computes a Sass value: ```scss @use "sass:math"; @function rem($pixels, $base: 16) { @return math.div($pixels, $base) * 1rem; } .title { font-size: rem(24); } ``` Use `math.div()` rather than slash division. Dart Sass 1.33.0 introduced `math.div()` and the deprecation warning for `/` division because CSS uses slash as a separator in valid syntax, including shorthand such as `font: 16px/1.5`. Saying slash division has already vanished everywhere is inaccurate; current 1.x can still warn for legacy cases. New code should not depend on that transition period. Mixins can quietly duplicate lots of CSS when included repeatedly. Inspect the compiled output. The source may be magnificently DRY while the delivered file is wearing six identical coats. Our [mixins versus functions guide](/sass-mixins-vs-functions/) gives a fuller decision tree. ## Conditionals, errors, and warnings Use `@if`, `@else if`, and `@else` when a mixin or function must choose among valid paths: ```scss @mixin button-variant($style: solid) { @if $style == solid { background: currentColor; } @else if $style == outline { border: 2px solid currentColor; } @else { @error "Unknown button style: #{$style}"; } } ``` `@error` prints a message and stack trace, stops compilation, and prevents silently wrong CSS. Use `@warn` for a non-fatal problem or deprecation in your own API; it reports the warning without stopping the whole build. ## Built-in modules and the color-function migration Load built-ins from their `sass:` modules: ```scss @use "sass:color"; @use "sass:map"; @use "sass:math"; ``` Then call functions through the namespace: ```scss @use "sass:color"; $brand: #5b3fd6; .button:hover { background: color.adjust( $brand, $lightness: -10%, $space: hsl ); } ``` This corrects two common stale-tutorial patterns. First, global versions of built-ins that live in modules, including the old global `percentage()` helper, have been deprecated since Dart Sass 1.80.0. Second, legacy single-channel color helpers including `darken()` and `lighten()` are deprecated as Sass adapts to CSS Color 4. Use current `sass:color` APIs and make the color space intentional where the operation requires it. Do not mechanically replace `darken($x, 10%)` with whichever new name looks adjacent. `color.adjust()` and `color.scale()` have different semantics. Compare the rendered result and contrast in every supported theme. ## Maps and loops: generate carefully Maps are useful for build-time token sets: ```scss @use "sass:map"; $spaces: ( "sm": 0.5rem, "md": 1rem, "lg": 2rem ); .stack { gap: map.get($spaces, "md"); } @each $name, $value in $spaces { .gap-#{$name} { gap: $value; } } ``` The loop produces three classes. That can be useful; a map with forty properties across six breakpoints can produce a small CSS weather event. Measure generated output and only emit utilities the project consumes. For nested maps, keep access paths named and test the generated design-token output. ## A project structure that can grow without ceremony Many Sass tutorials present the “7-1 pattern” as though every serious team graduates into exactly seven folders and one output file. It is one convention, not a Sass requirement. Start smaller: ```text scss/ ├── tokens/ │ ├── _colors.scss │ ├── _spacing.scss │ └── _index.scss ├── base/ │ ├── _reset.scss │ └── _type.scss ├── components/ │ ├── _button.scss │ └── _card.scss └── main.scss ``` Add `layout`, `pages`, `utilities`, `themes`, or vendor boundaries only when the codebase needs them. More folders do not make CSS scalable by osmosis. Clear ownership, limited output, stable module APIs, and predictable entrypoints do. Whether generated CSS belongs in Git is also a project decision. If deployment compiles assets, ignoring output can prevent stale artifacts. If a host or consumer expects committed CSS, omitting it breaks delivery. Document one rule in the repository and have CI verify the build. ## Migrate old Sass without rewriting it blindfolded Sass `@import` and global built-ins have emitted deprecation warnings since **Dart Sass 1.80.0**. Official docs say removal is planned for **Dart Sass 3.0.0**. In the current 1.x release, deprecated imports may still compile; describe them as deprecated, not already removed. For a real migration: 1. Commit or otherwise preserve a clean baseline. 2. Pin the compiler version and run the existing build and visual tests. 3. Run the official `sass-migrator` on an entrypoint with dependency migration enabled. 4. Review namespaces and generated CSS rather than accepting every automatic name. 5. Replace legacy global built-ins and color functions deliberately. 6. Upgrade in small steps and treat new deprecation warnings as work items. Run the official migrator on a branch and consult its current command reference for the exact entrypoint and dependency flags. If build tooling calls Sass through JavaScript, also check for the deprecated legacy `render()` or `renderSync()` API; the modern API uses `compile()`, `compileAsync()`, `compileString()`, and `compileStringAsync()`. ## Make compiler warnings fail somewhere useful A warning noticed in local development is helpful. The same warning repeated for six months in a noisy build log is decorative wallpaper. Capture the compiler version in the lockfile, keep one reproducible build command, and run it in continuous integration. Sass exposes CLI controls for deprecations, including options to make selected deprecations fatal, but add them deliberately: a dependency you do not control may warn before your own code does. Start by running the build with normal warnings and recording which messages come from first-party files and which come from dependencies. Fix your code. Update or replace dependencies through their documented releases. Only silence a dependency warning temporarily when you have an owner and removal date; otherwise “temporary” settles in, receives mail, and becomes infrastructure. Source maps deserve the same explicit policy. They are valuable during development because browser tools can point from generated CSS back to the relevant SCSS module. Production handling depends on whether maps are deployed publicly, uploaded privately to an error service, or omitted. The Dart Sass CLI offers `--no-source-map` and source-map URL controls. Pick a policy based on deployment rather than deleting every `.map` file because an old blog called them clutter. If mappings or generated rules look stale, delete the project's generated `dist/` directory and recompile from a clean source tree; never delete an unverified path. For example, a source map can turn a browser reference such as `main.css:812` into the useful origin `_button.scss:14`. That tiny translation is the difference between debugging the rule you wrote and touring the compiled file one numbered line at a time. Finally, compare output across compiler upgrades. A clean compile is necessary, but not sufficient: snapshot important generated CSS, run visual or component tests, and scan bundle size. Preprocessor migrations are unusually good at producing valid CSS that is nevertheless different CSS—the most polite kind of regression. ## Is Sass still worth using? Use Sass when its build-time features pay rent: configurable modules, code generation from maps, reusable mixins, custom functions, or a library API. Native CSS now covers custom properties and nesting, so a small site that wants only those features may be simpler without a preprocessing step. The decision is not ideological. Compile a representative entrypoint and inspect the output. Consider browser requirements, team familiarity, warning maintenance, source maps, build time, and whether runtime theming belongs in CSS. Sass remains useful; it no longer needs to be invited to every project merely because the invitation template says “front end.” Our honest take lives in the [Sass & SCSS hub](/sass-and-scss/). ## Quick reference | Feature | SCSS syntax | What reaches CSS | |---|---|---| | Variable | `$space: 1rem` | Value only | | Module | `@use "tokens"` | Module CSS, once | | Mixin | `@include focus-ring` | Declarations or rules | | Function | `rem(24)` | Returned value | | Conditional | `@if ... @else` | Chosen branch only | | Loop | `@each` | Generated rules | | Custom property | `--space: #{$space}` | Variable and value | ## Modern Sass checklist - Install one project-controlled implementation and record its version. - Invoke local Sass through npm scripts or `npx`. - Use `@use` and `@forward` in new code. - Keep module configuration explicit with `!default` and `with`. - Use namespaced built-ins such as `math.div()` and `map.get()`. - Replace deprecated color helpers with deliberate Color 4-aware operations. - Reserve CSS custom properties for runtime values. - Keep nesting shallow and inspect generated selectors. - Measure loops and repeated mixin output. - Read every deprecation warning before upgrading the compiler. The browser never sees how clever your SCSS was. It sees CSS. Keep that output boring, correct, and small; Sass can do the interesting work backstage. ## Sources checked - [Sass: current installation options](https://sass-lang.com/install/) - [Sass: SCSS and indented syntax](https://sass-lang.com/documentation/syntax/) - [Sass: Dart Sass command-line interface](https://sass-lang.com/documentation/cli/dart-sass/) - [Sass: @use](https://sass-lang.com/documentation/at-rules/use/) - [Sass: @forward](https://sass-lang.com/documentation/at-rules/forward/) - [Sass: @import and global built-in deprecation](https://sass-lang.com/documentation/breaking-changes/import/) - [Sass: Color 4 function changes](https://sass-lang.com/documentation/breaking-changes/color-functions/) - [Sass: legacy JavaScript API migration](https://sass-lang.com/documentation/breaking-changes/legacy-js-api/) - [Sass: Node Sass is end-of-life](https://sass-lang.com/blog/node-sass-is-end-of-life/) - [Sass: `@if` and `@else`](https://sass-lang.com/documentation/at-rules/control/if/) - [Sass: `@error`](https://sass-lang.com/documentation/at-rules/error/) - [Sass: `@warn`](https://sass-lang.com/documentation/at-rules/warn/) - [Sass: private-variable configuration deprecation](https://sass-lang.com/documentation/breaking-changes/with-private/) - [Sass: slash division and `math.div()`](https://sass-lang.com/documentation/breaking-changes/slash-div/) ## Frequently asked questions **How do I install Sass in a Node project?** Run `npm install --save-dev sass`, then invoke the project-local executable with an npm script or `npx sass`. A useful script is `sass scss/main.scss public/main.css`, with a second script adding `--watch` for development. Run `npx sass --version` to record what the project actually uses. The official Sass site also offers `sass-embedded`, standalone archives, and system-package options; choose one implementation deliberately rather than mixing global and local versions. **Should new Sass projects use @use or @import?** Use `@use` for modules and `@forward` to expose a curated public entrypoint. Sass `@import` has emitted deprecation warnings since Dart Sass 1.80.0 and is planned for removal in Dart Sass 3.0.0. It may still compile in the current 1.x release, so “deprecated” is not the same as “already removed.” Use the official Sass migrator and tests for an old codebase instead of performing a blind search-and-replace. **What is the difference between Sass variables and CSS custom properties?** A Sass variable such as `$space` is evaluated during compilation and disappears into the generated CSS. A CSS custom property such as `--space` remains in the browser, participates in the cascade, and can change by element, media query, theme, or JavaScript. Use Sass for build-time configuration and code generation; use custom properties for runtime values. To put a Sass value into a custom property, interpolate it: `--brand: #{$brand};`. **Should I use a Sass mixin or function?** Use a mixin when the reusable result is a group of declarations or rules; call it with `@include`. Use a function when the result is one Sass value used inside a declaration; return it with `@return`. Keep both small and named for intent. If a mixin emits a large block in many selectors, inspect the compiled CSS because reuse in source can still duplicate output. The browser downloads the result, not the elegance of your SCSS. **How should I divide numbers and adjust colors in modern Sass?** Load built-in modules explicitly. Use `@use 'sass:math'` with `math.div()` instead of slash division. Use `@use 'sass:color'` with Color 4-aware functions such as `color.adjust()` or `color.scale()` and state the color space where appropriate. Legacy helpers including `darken()` and `lighten()` are deprecated, as are global calls to built-ins that now live in modules. Read compiler warnings before they become upgrade-week fireworks. **Is Sass still useful now that CSS has nesting and custom properties?** Yes when a project benefits from build-time modules, configurable libraries, maps, loops, reusable mixins, or functions that generate static CSS. It may be unnecessary if the project only wanted variables and simple nesting, because modern CSS supplies runtime custom properties and native nesting. Choose by output and maintenance cost: compare the generated CSS, browser requirements, team familiarity, and tooling burden. Sass is a tool, not a mandatory front-end initiation ceremony. --- # CSS Animations Tutorial: Keyframes, Transitions, and Timing > A CSS keyframe animation combines an @keyframes rule with an animation declaration. Keyframes define property values at percentages from 0% to 100%; animation sets the name, duration, easing, delay, iterations, direction, fill mode, and play state. Use transitions for interpolation after a state change. Use keyframes for multiple stops, repetition, reversal, or sequences that start without a new state change. Source: https://thesassway.com/css-animations-tutorial/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-07 ## CSS keyframe animations in one minute A CSS keyframe animation has two parts: an `@keyframes` rule that defines property values at points in a sequence, and an `animation` declaration that assigns the sequence to an element. Use a transition when a property should interpolate after a state change such as `:hover`. Use keyframes for multiple stops, repetition, reversal or motion that starts without a new state change. Here is the smallest useful keyframe example: ```css @keyframes pulse { from { transform: scale(1); } to { transform: scale(1.05); } } .button { animation: pulse 600ms ease-in-out infinite alternate; } ``` `pulse` names the sequence. `600ms` is one iteration, `ease-in-out` controls progress within it, `infinite` repeats it, and `alternate` reverses every other iteration. The browser interpolates between `scale(1)` and `scale(1.05)`. ## When should you use a transition instead? A transition responds to a change between an element's old and new computed values. It needs a starting value, a changed value and a property that can transition. ```css .button { background: #2563eb; transition: background 200ms ease; } .button:hover, .button:focus-visible { background: #1e40af; } ``` The shorthand order is: ```css transition: ; ``` Several transitions can be comma-separated: ```css .card { transform: translateY(0); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); transition: transform 250ms ease-out, box-shadow 250ms ease-out; } .card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); } ``` Name the properties rather than using `transition: all`. `all` is valid, but it can animate a later property change that was never meant to move. | Transition property | Purpose | Example | |---|---|---| | `transition-property` | Properties to watch | `transform, opacity` | | `transition-duration` | Time from start to end | `200ms`, `0.3s` | | `transition-timing-function` | Progress curve | `ease`, `cubic-bezier(...)` | | `transition-delay` | Wait before starting | `100ms` | Seconds and milliseconds are equivalent units: `0.3s` equals `300ms`. As an editorial starting point, motion under roughly `100ms` reads as nearly immediate, many interface transitions fit between `150ms` and `400ms`, and a `500ms` hover can feel slow. Those are design starting points, not platform rules; test the actual interaction. ## How does the @keyframes rule work? The [CSS Animations Level 1 specification](https://www.w3.org/TR/css-animations-1/) defines keyframe selectors as percentages from `0%` through `100%`, with `from` and `to` as aliases for the endpoints. A multi-stop sequence looks like this: ```css @keyframes slide-in { 0% { opacity: 0; transform: translateX(-20px); } 70% { transform: translateX(4px); } 100% { opacity: 1; transform: translateX(0); } } .panel { animation: slide-in 400ms ease-out both; } ``` Multiple selectors can share declarations: ```css @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-16px); } } ``` If a property is missing from the `0%` or `100%` keyframe, the browser uses its underlying computed value for that endpoint when it can animate the property. That can be convenient, but explicit endpoints are easier to review when another rule later changes the base style. Keyframe declarations do not participate in the normal cascade in the same way as ordinary style rules. Do not use `!important` inside `@keyframes`; the animation specification says those declarations are ignored. ## What do the animation properties control? The familiar Level 1 shorthand represents eight longhands: | Property | Purpose | Example values | |---|---|---| | `animation-name` | Selects the `@keyframes` rule | `slide-in` | | `animation-duration` | Length of one cycle | `600ms`, `2s` | | `animation-timing-function` | Progress within a segment | `ease-out`, `linear` | | `animation-delay` | Offset before the active interval | `200ms`, `-200ms` | | `animation-iteration-count` | Number of cycles | `1`, `3`, `infinite` | | `animation-direction` | Playback direction | `normal`, `reverse`, `alternate` | | `animation-fill-mode` | Effect before or after playback | `none`, `forwards`, `both` | | `animation-play-state` | Runs or pauses the animation | `running`, `paused` | The shorthand grammar is flexible, but time values have a fixed interpretation: the first time is duration and the second is delay. ```css .notice { animation: slide-in 400ms ease-out 150ms 1 normal both; } ``` A duration cannot be negative. Its initial value is `0s`, which is a common reason correctly named keyframes appear to do nothing. A delay may be negative. `animation-delay: -200ms` starts immediately as if the animation had already run for 200 milliseconds; it does not wait a negative amount of wall-clock time. ## Why does an element snap back after the animation? `animation-fill-mode` controls when the animation effect applies outside its active interval: - `none` applies no effect before or after playback. - `forwards` keeps the effect at the point where the final iteration ended. - `backwards` applies the first relevant keyframe during a positive delay. - `both` combines `forwards` and `backwards`. This explains the snap, but `forwards` should not become a substitute for durable component state. If the final appearance represents a real open, closed, saved or selected state, set that state in the component's ordinary CSS and use animation only for the route between states. Direction also changes which endpoint counts as the beginning or end. With `reverse`, the `to` keyframe supplies the starting effect. With `alternate`, odd and even iterations run in opposite directions. ## How do timing functions work between keyframes? An easing function maps elapsed time to animation progress. These keywords cover most interface motion: | Value | Behavior | Common use | |---|---|---| | `linear` | Constant progress rate | Indeterminate spinners | | `ease` | Default cubic curve | General transitions | | `ease-in` | Slow start | An element leaving | | `ease-out` | Slow finish | An element entering | | `ease-in-out` | Slow start and finish | Reversing motion | Custom curves use `cubic-bezier(x1, y1, x2, y2)`. The [CSS Easing Functions Level 2 specification](https://www.w3.org/TR/css-easing-2/) requires both X coordinates to remain within `0` and `1`; the Y coordinates may fall outside that range. ```css .modal { transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1); } ``` `steps()` produces discrete jumps rather than continuous interpolation. A 20-character reveal can use 20 steps: ```css @keyframes typing { from { width: 0; } to { width: 20ch; } } .caption { width: 20ch; overflow: hidden; white-space: nowrap; animation: typing 2s steps(20) forwards; } ``` An `animation-timing-function` inside a keyframe controls the segment that starts at that keyframe and runs to the next one. This permits different easing on approach and departure without splitting the animation into separate names. ## Can CSS transition display: none now? `display` is discrete; there are no intermediate display values. [CSS Transitions Level 2](https://www.w3.org/TR/css-transitions-2/) introduces `transition-behavior: allow-discrete`, and `@starting-style` supplies a before-change style when an element was not previously rendered. ```css .popover { display: block; opacity: 1; transition: opacity 200ms ease, display 200ms allow-discrete; } .popover.is-closed { display: none; opacity: 0; } @starting-style { .popover { opacity: 0; } } ``` This is progressive CSS, not permission to assume every target browser implements the same draft feature set. Check support for the browsers in the project, and retain a usable fallback. `opacity` alone does not remove an element from layout or interaction, so an older fallback may also need `visibility`, `hidden` or script-managed state according to the component. ## Which properties should you animate for performance? Start with `transform` and `opacity`, then measure. Google's [high-performance animation guide](https://web.dev/articles/animations-guide) recommends checking rendering-pipeline cost before animating other properties because layout- or paint-triggering changes are harder to keep smooth. Do not turn that into a false guarantee. Whether an effect is composited depends on the browser, element and surrounding styles. Use browser developer tools to inspect frames, layout, paint and layer behavior on representative devices. Prefer this: ```css .box { transform: translateX(0); transition: transform 300ms ease; } .box.is-open { transform: translateX(240px); } ``` over animating `left` when the intended result is purely visual movement. Width, height, margin and positional properties can affect layout. Sometimes layout really must change; test it rather than disguising a semantic size change with a transform. `will-change` is a hint, not a required animation ingredient: ```css .menu { will-change: transform; } ``` The [W3C will-change specification](https://www.w3.org/TR/css-will-change/) explicitly warns against applying it across too many properties or elements because the optimizations consume resources. Add it only when measurement shows a need, apply it shortly before the change when practical, and remove it after the element stops changing. Our guides to [CSS custom properties](/css-variables-guide/) and [CSS gradients](/css-gradients/) cover two common inputs to animation systems. Keep duration and distance tokens named, but do not assume every custom property will interpolate; registration and the property's value type matter. ## How should reduced motion be implemented? The `prefers-reduced-motion` media feature has two values: `no-preference` and `reduce`. [Media Queries Level 5](https://www.w3.org/TR/mediaqueries-5/#prefers-reduced-motion) defines `reduce` as a request to remove or replace non-essential motion that can cause discomfort or distraction. The safest default is no transform animation, then opt into it only when no reduced-motion preference is expressed: ```css .feature-card { opacity: 1; transform: none; } @media (prefers-reduced-motion: no-preference) { .feature-card { animation: fade-up 500ms ease-out both; transition: transform 200ms ease, box-shadow 200ms ease; } .feature-card:hover, .feature-card:focus-visible { transform: translateY(-6px); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2); } } ``` Do not treat the popular global `0.01ms !important` reset as a complete accessibility solution. It can preserve a rapid flash of the same movement, override component behavior and interfere with code that listens for animation completion. Remove or replace non-essential motion deliberately, while keeping essential state feedback available without movement. [WCAG 2.2](https://www.w3.org/TR/WCAG22/) adds separate obligations that a media query does not erase. Moving, blinking or scrolling information that starts automatically, lasts more than five seconds and appears alongside other content needs a pause, stop or hide mechanism unless essential. Content must also stay within the three-flashes-or-below-threshold criterion. These are design and testing requirements, not optional polish. ## A complete accessible card example This preserves the original fade-and-lift example while making the static state the baseline: ```css @keyframes fade-up { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } } .feature-card { opacity: 1; transform: none; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); } @media (prefers-reduced-motion: no-preference) { .feature-card { animation: fade-up 500ms ease-out both; transition: transform 200ms ease, box-shadow 200ms ease; } .feature-card:hover, .feature-card:focus-visible { transform: translateY(-6px); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2); } } ``` Keyboard focus receives the same non-essential enhancement as hover. The card remains visible and understandable when animation does not run. ## Why is a CSS animation not working? Check these in order: 1. Confirm `animation-name` exactly matches an `@keyframes` name. 2. Inspect computed `animation-duration`; the initial value is `0s`. 3. Confirm the selector actually matches and is not inside an inactive media query. 4. Check whether the animated property can interpolate between the supplied values. 5. Add explicit `0%` and `100%` values to expose an unexpected underlying style. 6. Check whether `display: none`, DOM removal or a class change cancels the effect. 7. Inspect `animation-play-state`, iteration count, delay and reduced-motion rules. 8. Use the browser's animation and performance panels instead of judging only by eye. For transitions, confirm that the base style existed before the new style was applied. If both values arrive in the same style update, there may be no prior change for the browser to transition from. The durable rule is simple: transitions connect states; keyframes define sequences. Specify the endpoints, parse the shorthand deliberately, measure rendering cost, and design a complete experience for people who request less motion. ## Frequently asked questions **How do I write a CSS keyframe animation?** Define a named sequence with @keyframes, using from and to or percentages between 0% and 100%. Apply it with animation, for example animation: slide-in 400ms ease-out both. The first time in the shorthand is duration; a second time is delay. Explicit endpoints make the sequence easier to debug. **What is the difference between a CSS transition and animation?** A transition interpolates when a property changes between an old and new state, such as hover or a toggled class. A keyframe animation runs a named sequence that can contain multiple stops, repeat, reverse, pause, and begin without a new state change. Use the simpler transition when two states are enough. **Why is my CSS animation not working?** First match animation-name to the @keyframes name and inspect computed animation-duration, whose initial value is 0s. Then confirm the selector and media query match, the values can interpolate, and display or DOM removal is not cancelling the effect. Check delay, iteration count, play state, and reduced-motion rules in developer tools. **What does animation-fill-mode: forwards do?** It keeps the animation effect at the point where its final iteration ended after playback. Backwards applies the relevant starting keyframe during a positive delay, and both combines the two. Fill mode does not change the component's durable base state; encode a real open, closed, or selected state in ordinary CSS. **Which CSS properties are best for animation performance?** Start with transform and opacity, then measure on representative devices. Properties that trigger layout or paint are harder to keep smooth, but compositor behavior is not guaranteed by a property name alone. Use browser performance tools to inspect frames, layout, paint, and layers before adding will-change or rewriting a semantic size change. **How do I respect prefers-reduced-motion?** Make the static, fully usable state the default and place non-essential transform animation inside @media (prefers-reduced-motion: no-preference). Alternatively, replace motion deliberately inside the reduce query while retaining state feedback. A global 0.01ms reset is not complete accessibility work; also test pause controls and flash limits where WCAG applies. --- # The CSS Box Model: Margin, Border, Padding, and Content > The CSS box model describes a box as content surrounded by padding, border, and margin. With the default content-box sizing, declared width covers the content and padding and border are added outside it. With box-sizing: border-box, width includes content, padding, and border. Margin remains outside either calculation. Backgrounds paint through padding and, by default, underneath the border; margins are transparent. Source: https://thesassway.com/css-box-model/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-07 ## The CSS box model in one sentence Every rendered element generates one or more boxes. For a basic block box, the **CSS box model** describes four nested areas: **content**, **padding**, **border**, and **margin**. The `box-sizing` property decides whether a declared `width` or `height` measures the content box or the border box. The [CSS Box Model specification](https://www.w3.org/TR/css-box-4/) defines the same inside-to-outside order: | Area | What it contains | Can affect occupied space? | Background behavior | |---|---|---|---| | **Content** | Text, images, and child boxes | Sized by content and properties such as `width` | Background is painted here | | **Padding** | Space between content and border | Yes | Background is painted here | | **Border** | The border surrounding content and padding | Yes | Background is painted underneath it by default; the border paints over that | | **Margin** | Transparent space outside the border | Yes, through separation or overlap | Always transparent | The order does not change. What does change is which edge a sizing property measures, how an element participates in layout, and whether adjoining margins collapse. ## The four areas, precisely ### Content The content box holds the element's text, image, or descendant boxes. With `box-sizing: content-box`, a declared `width` and `height` size this area. If a normal block box has `width: auto`, it generally fills the available inline space after its margins, borders, and padding are accounted for; that is more precise than saying every block is simply `100%` wide. ### Padding Padding creates space between the content and border. It belongs to the element, so the element's background and hit area extend through it. ```css .button { padding: 12px 20px; /* top/bottom, then left/right */ } ``` The shorthand accepts one to four values in top, right, bottom, left order. Physical longhands include `padding-top`; logical alternatives include `padding-block` and `padding-inline`. Padding cannot be negative. One percentage trap is worth learning early. Under the current [CSS Box Model specification](https://www.w3.org/TR/css-box-4/#padding-physical), percentage padding on any side refers to the containing block's **logical width**. That means `padding-top: 10%` is not normally ten percent of the containing block's height. ### Border A border occupies the area between padding and margin. The shorthand accepts width, style, and color: ```css .card { border: 2px solid #d0d0d0; } ``` The initial `border-style` is `none`, so setting only `border-width` does not produce a visible border. Border width still participates in box sizing when a border is present. Other border styles include `dashed` and `dotted`; the key point is that a non-`none` style must participate before the border is painted. ### Margin Margin is transparent space outside the border. It may be positive, zero, negative, or `auto`. Negative margin can pull boxes together or make them overlap; it does not create negative padding. For example, `margin-top: -10px` shifts a box upward in the common horizontal writing mode; whether it overlaps another box depends on the surrounding layout. `margin: 0 auto` can center a normal block horizontally when its used width is narrower than the available inline space. It is not a universal centering command: an auto-width block already fills the available space, and other layout modes resolve auto margins by their own rules. ## Margin or padding? Use the border edge as the decision line: - Use **padding** when the space belongs inside the element's border, should show its background, or should enlarge the hit area of an interactive element. - Use **margin** when the space separates this box from neighboring boxes and should remain transparent. | Question | Margin | Padding | |---|---|---| | Inside or outside the border? | Outside | Inside | | Shows the element's background? | No | Yes | | Can be negative? | Yes | No | | Part of an element's clickable area? | No | Yes | | Can collapse in normal block layout? | Block-axis margins can | No | For example, padding makes an entire button-style link easier to click and gives its hover background room. Margin separates that link from the next item. For the lengths used in these declarations, see the [CSS units guide](/css-units-guide/). ## What `box-sizing` changes The initial value of `box-sizing` is `content-box`. In that model, declared width applies to the content box and padding and border are added outside it. ```css .box { width: 300px; padding: 20px; border: 5px solid black; /* Border-box width = 300 + 20 + 20 + 5 + 5 = 350px */ } ``` With `border-box`, the declared width includes content, padding, and border: ```css .box { box-sizing: border-box; width: 300px; padding: 20px; border: 5px solid black; /* Border-box width = 300px; content width = 250px */ } ``` Margin is excluded in both modes. A `300px` border-box with `10px` left and right margins occupies `320px` in an ordinary non-collapsing horizontal calculation. | | `content-box` | `border-box` | |---|---|---| | Declared `width` measures | Content | Content + padding + border | | Example border-box width | `350px` | `300px` | | Adding padding while width stays fixed | Grows border box | Reduces content space | | Margin included? | No | No | MDN's current [`box-sizing` reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/box-sizing) also records an exception to the usual-default shorthand: browsers normally style ``, `` types with `border-box`. Inspect a form control's computed value instead of assuming `content-box`. ## Use a reset that pseudo-elements inherit Applying `border-box` globally is a common way to make component sizing predictable. This form lets a component override the value on its root and pass that choice to descendants and pseudo-elements: ```css html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } ``` MDN marks `box-sizing` as widely available across browsers, but production code should still be checked against its actual support targets. The direct version also remains valid: ```css *, *::before, *::after { box-sizing: border-box; } ``` `border-box` does not prevent all overflow. Content can still have an intrinsic minimum size, a long unbreakable string can still overflow, and an item's margins can still push its margin box beyond a container. Treat `box-sizing` as one sizing rule, not an overflow switch. ## Margin collapsing without the shortcut errors In normal block layout, adjoining **block-axis** margins can collapse into one margin. In the common horizontal writing mode, these are the top and bottom margins. Inline-axis margins do not collapse. ```css .top { margin-bottom: 30px; } .bottom { margin-top: 20px; } /* The gap is 30px, not 50px. */ ``` "The larger margin wins" is correct only when the adjoining margins are positive. MDN's [box-model guide](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Box_model#margin_collapsing) gives the complete arithmetic: - Among positive margins, use the largest. - Among negative margins, use the one farthest from zero. - When positive and negative margins adjoin, subtract the largest absolute negative value from the largest positive value. Collapsing can occur between adjacent block siblings, between a parent and its first or last in-flow block child when nothing separates their margins, and through an empty block with no relevant border, padding, height, or in-flow content. It does not occur between flex or grid items, and it does not cross a new formatting-context boundary. A parent can deliberately prevent child margins from escaping by using `display: flow-root`. MDN's [block formatting context reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Display/Block_formatting_context) also notes that `overflow` values other than `visible` **and `clip`** create a block formatting context. Prefer `flow-root` when containment is the actual intent; changing overflow may introduce clipping or scrollbars. Other deliberate spacing patterns also survive: put padding or a border between parent and child margins, use Flexbox or Grid with `gap`, or assign vertical rhythm in one direction instead of setting both adjoining margins. ## Inline boxes use only part of the model The model still describes inline boxes, but non-replaced inline elements such as `` and ordinary `` elements do not behave like block boxes: - `width`, `height`, `margin-top`, and `margin-bottom` do not affect a non-replaced inline box in normal inline layout. - Left and right padding, border, and margin move surrounding inline content. - Top and bottom padding and border are painted, but do not move the surrounding lines; they can overlap content on adjacent lines. That is the corrected version of the tempting but inaccurate statement that "vertical margin is applied visually." For non-replaced inline elements, top and bottom margins have no effect. MDN demonstrates these rules in its [current box-model lesson](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Box_model#the_box_model_and_inline_boxes). Use `display: inline-block` when an element should remain in the inline flow but needs block-like width, height, padding, border, and margin behavior: ```css .button-link { display: inline-block; padding: 12px 20px; } ``` Replaced inline elements such as images have their own sizing behavior, so do not generalize the non-replaced `` rules to every inline-level box. ## Background painting extends under the border The element background is visible through the content and padding areas. By default, it is also painted underneath the border to the border edge; an opaque border simply covers that paint. Margin remains transparent. That distinction matters with transparent or partly transparent borders. Use `background-clip` when the paint must stop at a different edge: ```css .card { border: 5px solid rgb(0 0 0 / 30%); background: white; background-clip: padding-box; } ``` This corrects the common simplification that a background always "stops at the border." The [CSS Box Model specification](https://www.w3.org/TR/css-box-4/#padding) states that backgrounds are additionally painted underneath the border by default. ## Debug the number the browser actually used Chrome, Edge, and Firefox DevTools all expose a box-model diagram, though tab names and colors can change. Inspect the element and check these in order: 1. Read the computed `box-sizing`, `width`, `min-width`, and `max-width`. 2. Read padding and border on both relevant sides. 3. Check margin separately; it is outside the border-box calculation. 4. Confirm the element's outer and inner `display` behavior. 5. If spacing is vertical, check whether margins collapse or a formatting context prevents collapse. 6. If a percentage padding surprises you, calculate it from the containing block's logical width. For visual debugging without changing layout, an outline is useful because it does not consume box-model space: ```css * { outline: 1px solid rgba(255, 0, 0, 0.3); } ``` Remove the rule before shipping. If a box still appears wider than the arithmetic predicts, inspect intrinsic content, transforms, positioned descendants, and overflow rather than adding compensating negative margins. ## The working checklist - Read boxes from content to padding to border to margin. - Use padding for interior space and margin for separation. - Remember that backgrounds paint under the border by default; margins stay transparent. - Choose `border-box` when declared dimensions should include padding and border. - Keep margin outside every `box-sizing` calculation. - Treat margin collapsing as a block-formatting-context rule, including its negative-margin arithmetic. - Distinguish non-replaced inline boxes from block and replaced boxes. - Use logical properties such as `padding-inline` when spacing should follow writing mode. - Inspect computed values before changing the CSS. The [beginner's CSS guide](/what-is-css/) supplies the wider cascade and layout context. Then use [how to center a div](/how-to-center-a-div/) to apply box sizing, auto margins, Flexbox, and Grid to a real layout problem. ## Frequently asked questions **What is the CSS box model?** The CSS box model is the sizing and spacing system for boxes generated by elements. A basic block box has four nested areas: content, padding, border, and margin. Content holds text or descendants, padding separates content from the border, the border surrounds both, and transparent margin sits outside. The box-sizing property determines which inner areas a declared width or height includes. **What is the difference between margin and padding?** Padding is inside the border, so the element's background and interactive hit area extend through it; negative padding is invalid. Margin is transparent space outside the border. It may be negative or auto and can collapse with adjoining block-axis margins in normal block layout. Use padding for interior spacing and margin for separation from neighboring boxes. **What does box-sizing: border-box do?** It makes a declared width or height include the content, padding, and border. If a box is 300px wide with 20px padding and a 5px border on each side, border-box keeps the border box at 300px and leaves 250px for content. Margin is still outside. With content-box, the same declarations produce a 350px border box. **Does margin count toward an element's width?** Margin is not included in the declared width under either box-sizing value. A 300px border-box element with 10px left and right margins occupies 320px in an ordinary horizontal, non-collapsing calculation. Padding and border can be included in the 300px by border-box; margin remains outside the border edge. Layout context can still change how auto or collapsing margins resolve. **What is margin collapsing in CSS?** Adjoining block-axis margins can combine in normal block layout. With two positive margins, the larger value is used, so 30px and 20px produce 30px, not 50px. Negative margins require different arithmetic: the largest absolute negative is subtracted from the largest positive. Flex and Grid items do not collapse margins, and display: flow-root creates a boundary that suppresses collapse. **Why is my element bigger than the width I set?** First inspect its computed box-sizing. Under content-box, padding and border are added to the declared content width: 300px plus 20px padding and a 5px border on both sides equals 350px. Border-box includes those areas in 300px. If the arithmetic still disagrees, inspect margins, intrinsic minimum sizes, unbreakable content, transforms, and the containing layout. **Does an element's background include its border?** By default, the background is painted through the content and padding areas and continues underneath the border to the border edge. An opaque border covers that part of the background; a transparent border can reveal it. Margin is always transparent. Use background-clip to stop background paint at the padding edge or content edge when that is the intended result. **Why do width and vertical margins not work on a span?** A typical span is a non-replaced inline box. In normal inline layout, width, height, margin-top, and margin-bottom have no effect. Top and bottom padding and borders paint but do not move adjacent lines, so overlap is possible. Set display: inline-block when the element should remain inline while respecting width, height, padding, border, and margins like a block box. --- # CSS Cascade Layers: Control Priority with @layer > CSS cascade layers group styles into an explicit priority order with `@layer`. For normal author declarations, later layers beat earlier layers before selector specificity is compared; unlayered normal styles beat every layered normal style. Important declarations reverse the layer order. Declare the complete stack once near the top, place vendor and application CSS deliberately, and keep selectors simple. Layers control precedence between groups while specificity still resolves conflicts inside a layer. Source: https://thesassway.com/css-cascade-layers/ Publisher: The Sass Way Published: 2026-08-25 Updated: 2026-08-25 ## Cascade Layers Let You Set Priority Before Selectors Compete CSS cascade layers use `@layer` to group styles into an explicit priority order. For normal author styles, a rule in a later layer beats a conflicting rule in an earlier layer even when the earlier selector has higher specificity. Declare the order once, then write modest selectors inside each layer instead of escalating with IDs, deeper nesting, or `!important`. Here is the core pattern: ```css @layer reset, vendor, base, components, utilities, overrides; @layer base { a { color: rebeccapurple; } } @layer utilities { .text-blue { color: royalblue; } } ``` An anchor with `class="text-blue"` is royal blue. The class is more specific than the type selector, but that is not why it wins: `utilities` was declared after `base`. The browser compares origin and importance, then layer priority, and only then specificity among rules that remain in the same layer. ## What Problem Cascade Layers Solve Without layers, all normal author rules share one broad arena. A component selector may need to override a framework selector, which leads to a more specific selector. A utility then needs to override the component, so it grows another class. Soon a one-line color change requires a selector with three ancestors and an `!important` flag. Layers separate those concerns before selector weight matters. You can assign reset styles low priority, third-party CSS above them, ordinary base styles next, components above base, and intentional utilities or overrides last. The selector inside a later layer can stay simple. This does not replace understanding the cascade. It adds a priority step to it. If two declarations are in the same origin, importance bucket, and layer, specificity and source order still decide the winner. Read our [CSS specificity guide](/css-specificity-explained/) if that part of the decision is still fuzzy. ## The Priority Rules, Exactly For normal author declarations: 1. The first layer created has the lowest layer priority. 2. Each subsequently created layer has higher priority. 3. Normal declarations outside every layer beat normal declarations inside all named and anonymous layers. 4. Within one layer, normal specificity and source-order rules apply. MDN's [`@layer` reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@layer) documents those rules and the three declaration forms: a statement that establishes names, a named block containing rules, and an anonymous block. The unlayered rule surprises people, so see it directly: ```css @layer components { #checkout .button { background: seagreen; } } .button { background: tomato; } ``` The button becomes tomato. The unlayered `.button` beats the layered `#checkout .button` for a normal author declaration, despite dramatically lower specificity. That behavior is useful for intentional last-resort overrides, but it can also puncture your architecture. Once a project adopts layers, either keep ordinary application CSS inside them or reserve unlayered CSS for a clearly documented purpose. ## Declare the Entire Order at the Top The safest setup is a single statement near the top of the entry stylesheet: ```css @layer reset, vendor, base, components, utilities, overrides; ``` This creates all six layers and fixes their order. Later, files can reopen any named layer: ```css @layer components { .card { border: 1px solid #d8dee9; } } ``` ```css @layer base { body { color: #1f2937; } } ``` Although the `base` block appears later in this example, `components` still has higher layer priority because the opening statement created `base` first and `components` later. Reusing a name appends rules to that existing layer; it does not move the layer. This is the architectural payoff: import order can change without silently redefining the intended precedence, provided the layer order statement remains first. ## A Practical Layer Stack | Layer | Put this inside | Keep this out | |---|---|---| | `reset` | Box sizing, margin normalization, sensible element defaults | Brand styling and component states | | `vendor` | Framework or third-party stylesheet rules | Local fixes masquerading as vendor code | | `base` | Typography, links, document-level defaults | One-off page exceptions | | `components` | Buttons, cards, navigation, form controls | Atomic utilities | | `utilities` | Single-purpose helpers | Complex components with hidden side effects | | `overrides` | Temporary, explicit exceptions | The entire feature stylesheet | Names are conventions, not reserved words. A small project may need only `base`, `components`, and `utilities`. A design system may prefer `tokens`, `reset`, `elements`, `objects`, `components`, and `utilities`. Choose names that describe responsibility and write the order down once. Do not create a layer for every file. Layers express precedence groups, not folder structure. Twenty micro-layers make priority harder to understand than the specificity problem they were meant to solve. ## Import Third-Party CSS into a Layer The `layer()` function on `@import` assigns an imported stylesheet to a layer: ```css @layer reset, vendor, base, components, utilities; @import url("vendor.css") layer(vendor); ``` Now normal rules in `base`, `components`, or `utilities` can override conflicting vendor rules without copying the vendor's selector weight. The CSS Cascade Level 5 specification lists [`@import` with `layer()`](https://www.w3.org/TR/css-cascade-5/#layer-order) as one way to assign imported content to a layer. Keep `@import` placement legal: it belongs before ordinary style rules. A layer-order statement may precede it, which lets you establish the stack before assigning the import. If your build tool bundles files, inspect the emitted CSS. You want one coherent layer order followed by rules assigned to those names. A build step that strips unknown at-rules or unwraps blocks incorrectly destroys the architecture. Test the compiled artifact, not just the source tree. ## Anonymous and Nested Layers An anonymous layer has no reusable name: ```css @layer { .legacy-widget { font: inherit; } } ``` It receives a position when created, but you cannot reopen it later by name. That makes anonymous layers reasonable for isolated one-off content and poor for an application structure spread across files. Named layers can also contain nested layers: ```css @layer components { @layer defaults, variants; @layer defaults { .button { background: slategray; } } @layer variants { .button--primary { background: royalblue; } } } ``` Here, `components.variants` has higher normal priority than `components.defaults`, but the whole `components` group still occupies its declared position relative to `base` and `utilities`. Nested layers are useful inside a distributed design system. For an ordinary site, keep the hierarchy shallow. If developers need a map beside the monitor to change a button, the CSS has won a battle nobody asked it to fight. ## The `!important` Order Is Reversed This is the rule most likely to cause a confident mistake. For important declarations inside layers, the layer order reverses: an important declaration in an earlier layer beats an important declaration in a later layer. Layered important declarations also outrank unlayered important declarations in the same author origin. ```css @layer base, utilities; @layer base { .notice { color: darkred !important; } } @layer utilities { .text-blue { color: royalblue !important; } } ``` An element with both classes becomes dark red, because `base` was created before `utilities` and important layer priority runs in the opposite direction. The reversal protects low-level contracts. A reset or foundational layer can mark a truly critical rule important without a later convenience layer casually defeating it. MDN's [cascade-layer tutorial](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers) lays out the normal order and its inverted important order. Do not use this as a reason to add `!important` everywhere. Use layers to reduce importance battles. When an important declaration is genuinely required, document why it belongs in that layer. ## Cascade Layers with Sass and SCSS `@layer` is CSS syntax, so it can wrap ordinary SCSS nesting: ```scss @layer components { .button { padding: 0.75rem 1rem; &:hover { background: royalblue; } } } ``` Keep the master order in one entry file and place partial contents inside the intended layer. Do not let each partial invent its own global order statement. Our [complete Sass guide](/complete-guide-to-sass/) covers the module system for loading partials, and our comparison of [native CSS and Sass nesting](/css-nesting-vs-sass-nesting/) explains how the selector syntax differs from layer priority. Be especially careful when a Sass module emits CSS as a side effect. The module may load once, but wherever its emitted rules land must still match the layer architecture you intend. Compile a representative entry point and inspect the final order. ## Migrating an Existing Stylesheet Do not wrap the entire codebase in six layers all at once. Migrate in visible, testable steps: 1. **Inventory sources.** List resets, vendor CSS, global rules, components, utilities, and emergency overrides. 2. **Declare a small order.** Start with three or four broad layers. 3. **Layer third-party CSS first.** It usually delivers the clearest immediate benefit. 4. **Move one local concern at a time.** Run visual regression tests after each move. 5. **Find unlayered rules.** Decide whether each is intentional or simply missed. 6. **Audit `!important`.** Remember the reversed order before relocating any important declaration. 7. **Remove obsolete specificity hacks.** Simplify selectors only after the layer behavior is verified. Use browser developer tools to inspect crossed-out declarations and confirm which layer wins. A rule losing with higher specificity is not necessarily a bug; it may be the exact architecture you declared. ## Common Mistakes ### Declaring layer names in accidental order The first creation fixes the order. Put a complete statement at the top instead of relying on whichever file happens to load first. ### Leaving most app styles unlayered Unlayered normal rules outrank every layered normal rule. A single forgotten stylesheet can appear to “break” the stack while behaving exactly according to the cascade. ### Expecting layers to beat inline styles or origins universally Layers operate within an origin and importance context. They are not a top-level override switch for every style source. Keep origin, importance, layer, specificity, and source order conceptually separate. ### Moving `!important` rules without retesting Normal layer order and important layer order run in opposite directions. A migration that preserves normal behavior can invert important behavior. ### Using layers as permission for chaotic selectors Layers control precedence across groups. Inside one layer, a deeply specific selector is still deeply specific. Keep selectors modest and component boundaries clear. ## The Short Version to Remember Declare layers once from low to high priority, place related CSS inside them, and keep routine application styles out of the unlayered space. Later normal layers beat earlier normal layers before specificity is compared. Important declarations reverse that layer order. With that model in place, `@layer` turns the cascade from an accidental result of file order into an explicit part of the stylesheet's architecture. ## Frequently asked questions **Do CSS cascade layers replace specificity?** No. Layers add a decision before specificity. When conflicting normal declarations are in different layers, the higher-priority layer wins and selector specificity across those layers does not change that result. When declarations compete inside the same layer, the usual specificity and source-order rules still apply. Layers make broad architectural priority explicit; they do not make selector design irrelevant. **Do unlayered CSS rules override layered rules?** For normal author declarations, yes: unlayered styles have higher priority than normal styles in every named or anonymous layer. This is useful for a deliberately reserved final override, but dangerous when a stylesheet is accidentally left outside the architecture. Important declarations behave differently: layered important declarations outrank unlayered important declarations, and earlier layers have higher important priority than later layers. **Why does !important reverse cascade layer order?** The reversed order protects foundational constraints from later convenience layers. For important declarations, an earlier layer outranks a later layer, opposite the order for normal declarations. That means an important rule in a low-level reset or base layer cannot be casually defeated by an important utility added later. The reversal is intentional, but it makes migrations risky unless every important declaration is retested. **Can I put imported CSS in a cascade layer?** Yes. Use the `layer()` function on `@import`, such as `@import url("vendor.css") layer(vendor);`. Keep imports before ordinary style rules; a layer-order statement may appear first. Assigning vendor CSS to a lower-priority layer lets later application layers override its normal rules without copying high-specificity selectors. Inspect the built CSS to ensure your bundler preserves the layer and its declared order. **Can Sass use CSS cascade layers?** Yes. `@layer` is native CSS syntax and can contain SCSS rules, including Sass nesting. Keep the global layer-order statement in a clear entry point, assign emitted CSS to deliberate layers, compile the project, and inspect the output. Sass modules and layers solve different problems: modules organize loading and names, while cascade layers control the priority of the CSS that reaches the browser. **What order should CSS layers use?** A practical low-to-high normal order is `reset`, `vendor`, `base`, `components`, `utilities`, and `overrides`, but the names are not reserved and smaller projects need fewer groups. The important part is to declare the whole order once and give each layer a clear responsibility. Avoid making one layer per file, and reserve the final override layer for genuine exceptions rather than routine feature styling. --- # CSS clamp() Explained: Fluid Typography Without Breakpoints > CSS clamp() takes three values — clamp(min, preferred, max) — and returns the preferred value as long as it stays between the minimum and maximum. For fluid typography, you set a rem-based floor and ceiling with a rem + vw expression in the middle, like font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem). Text then scales smoothly with the viewport, without media queries, while never becoming too small or too large. Source: https://thesassway.com/css-clamp-fluid-typography/ Publisher: The Sass Way Published: 2026-08-07 Updated: 2026-08-07 ## What Does CSS clamp() Do? CSS `clamp()` takes three values — a minimum, a preferred value, and a maximum — and returns the preferred value as long as it stays between the two limits. Written as `clamp(min, preferred, max)`, it lets a font size (or any length) scale fluidly with the viewport while never dropping below your floor or growing past your ceiling. That one function replaces the most common reason developers write media queries: stepping font sizes up at each breakpoint. Instead of a 16px body size that jumps to 18px at 768px and 20px at 1200px, you write a single declaration and the browser interpolates smoothly through every viewport in between. ```css h1 { font-size: clamp(2rem, 1rem + 4vw, 4rem); } ``` That heading is never smaller than `2rem`, never larger than `4rem`, and glides between the two as the viewport changes. No breakpoints, no sudden jumps, no awkward in-between screens where the type looks wrong. ## The Syntax: Minimum, Preferred, Maximum `clamp()` is shorthand for a pattern you could build yourself with nested `min()` and `max()` functions. These three declarations are equivalent: ```css font-size: clamp(2rem, 1rem + 4vw, 4rem); font-size: max(2rem, min(1rem + 4vw, 4rem)); font-size: min(max(2rem, 1rem + 4vw), 4rem); ``` The three arguments each play a distinct role: - **Minimum** — the floor. On small screens, where the preferred calculation would produce something tiny, this value wins. - **Preferred** — the fluid part. This is usually a `calc()`-style expression mixing `rem` and `vw`. Inside `clamp()` you can write math directly without wrapping it in `calc()`. - **Maximum** — the ceiling. On huge monitors, this stops your headline from becoming a billboard. One gotcha worth knowing: if your minimum is accidentally larger than your maximum, the browser resolves the expression in order — `clamp()` behaves like `max(MIN, min(VAL, MAX))` — so the minimum wins. Nothing errors, your text just quietly stops scaling, which can be confusing to debug. ## Building a Fluid Font Size Step by Step The preferred value is where the real design decision lives. You are defining a straight line: at some small viewport the text should be size A, and at some large viewport it should be size B. Everything between is linear interpolation. Say you want body text at **16px on a 400px viewport** scaling to **20px on a 1200px viewport**. Two steps: **1. Find the slope** — how many pixels of font size you gain per pixel of viewport: ``` slope = (20 − 16) / (1200 − 400) = 4 / 800 = 0.005 ``` Multiply by 100 to express it in `vw`: `0.5vw`. **2. Find the intercept** — the font size the line would have at a 0px viewport: ``` intercept = 16 − (0.005 × 400) = 16 − 2 = 14px = 0.875rem ``` Put it together: ```css body { font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem); } ``` Sanity check the endpoints: at 400px wide, `0.5vw` is 2px, and 14 + 2 = 16px. At 1200px wide, `0.5vw` is 6px, and 14 + 6 = 20px. The line passes exactly through both targets, and `clamp()` locks it flat outside that range. If you'd rather not do the algebra by hand, tools exist that generate the expression from your four inputs — or you can let Sass do it, as shown below. ## Why the Preferred Value Should Mix rem and vw You will sometimes see fluid type written as pure viewport units: `font-size: 3vw`. Avoid this, for two reasons. **Accessibility.** Users who raise their browser's default font size expect text to get bigger. A `rem`-based value respects that preference; a pure `vw` value ignores it entirely, and text-only zoom in some browsers won't scale it either. Mixing a `rem` term into the preferred value (`0.875rem + 0.5vw`) means user preferences still move the needle, and the `rem`-based minimum and maximum guarantee sensible bounds. When you're done, verify your text can still roughly double in size when zoomed — that's the behavior accessibility guidelines expect. **Control.** A pure `vw` value scales from zero to infinity with nothing anchoring it to your type scale. The `rem + vw` form gives you an explicit line through two chosen points instead of a ratio you found by eyeballing. If the difference between `rem`, `em`, and viewport units is still fuzzy, our [guide to CSS units](/css-units-guide/) walks through exactly when each one earns its place. ## clamp() vs min() vs max() The three comparison functions ship together and get confused constantly. Here's the practical breakdown: | Function | What it returns | Typical use | |---|---|---| | `min(a, b)` | The **smaller** value | Cap growth: `width: min(90%, 60rem)` | | `max(a, b)` | The **larger** value | Enforce a floor: `font-size: max(1rem, 2vw)` | | `clamp(min, val, max)` | The middle value, bounded both ways | Fluid type and spacing with hard limits | The naming trips people up: `min()` sets a *maximum* (it never lets the value exceed the smaller argument), and `max()` sets a *minimum*. `clamp()` does both at once, which is why it's the default choice for typography. All three accept any length-producing expression, work in nearly every property that takes a length, and are supported in all modern browsers — `clamp()` landed across Chrome, Edge, Firefox, and Safari in the first half of 2020, so unless you support very old browsers you can use it freely. ## Let Sass Do the Math Computing slopes and intercepts by hand gets old after the second heading level. A small Sass function turns the whole formula into one readable call: ```scss @use "sass:math"; @function fluid($min, $max, $min-vw: 25rem, $max-vw: 75rem) { $slope: math.div($max - $min, $max-vw - $min-vw); $intercept: $min - $slope * $min-vw; @return clamp(#{$min}, #{$intercept} + #{$slope * 100}vw, #{$max}); } h1 { font-size: fluid(2rem, 4rem); } ``` Compiled with Dart Sass, that produces: ```css h1 { font-size: clamp(2rem, 1rem + 4vw, 4rem); } ``` Everything stays in `rem` — including the viewport arguments (`25rem` = 400px, `75rem` = 1200px at the browser default) — so the units cancel cleanly in the division. Note the `math.div()` call: modern Sass deprecated the `/` operator for division, so this is the correct, future-proof form. Drop the function in a shared partial, load it with `@use`, and your whole type scale becomes calls like `fluid(1rem, 1.25rem)` and `fluid(1.5rem, 2.5rem)`. The same function works for fluid spacing, too: `padding: fluid(1rem, 3rem)` gives sections breathing room that grows with the viewport. ## When You Still Need Media Queries `clamp()` removes breakpoints from *typography*, not from responsive design. You still want [media queries](/css-media-queries/) when: - **Layout changes structurally.** A sidebar collapsing under the content, a nav becoming a hamburger — those are discrete state changes, not smooth interpolations. - **The type scale itself should change.** Sometimes a mobile design wants a genuinely different hierarchy, not just smaller sizes. - **You're adjusting for capability, not width.** `prefers-reduced-motion`, `prefers-color-scheme`, and hover support have no fluid equivalent. The winning combination in most modern codebases: `clamp()` for text and spacing, media or container queries for structural layout shifts. That pairing fits naturally with a [mobile-first workflow](/mobile-first-css/) — your unqueried base styles are already fluid, so the queries you do write are fewer and only handle real layout changes. ## A Complete Fluid Type Scale Here's a compact, copy-pasteable scale using custom properties, sized to run from a 400px to a 1200px viewport: ```css :root { --step-0: clamp(1rem, 0.875rem + 0.5vw, 1.25rem); /* body: 16 → 20px */ --step-1: clamp(1.25rem, 1rem + 1vw, 1.75rem); /* h3: 20 → 28px */ --step-2: clamp(1.5rem, 1.125rem + 1.5vw, 2.25rem); /* h2: 24 → 36px */ --step-3: clamp(2rem, 1rem + 4vw, 4rem); /* hero: 32 → 64px */ } body { font-size: var(--step-0); } h1 { font-size: var(--step-3); } ``` Two practical tips as you build your own: - **Let large sizes scale harder than small ones.** Body text barely needs to move (16 → 20px here); display headings can swing dramatically (32 → 64px). That widening contrast between levels is what makes fluid scales feel designed rather than merely shrunk. - **Test the extremes.** Drag your browser from its narrowest width to full-screen on your largest monitor. The clamps should engage before anything looks broken. Fluid typography is one piece of the larger discipline of building layouts that work everywhere — for the full picture, from viewport meta tags to responsive images, see our [complete responsive design guide](/responsive-design-guide/). The short version to remember: `clamp(min, rem + vw, max)`. Floor, fluid line, ceiling. Once the formula clicks, you'll wonder why you ever maintained five breakpoints just to resize an `h1`. ## Frequently asked questions **What do the three values in clamp() mean?** The first value is the minimum (the floor your value never drops below), the second is the preferred value (usually a fluid expression like 0.875rem + 0.5vw that scales with the viewport), and the third is the maximum (the ceiling). The browser uses the preferred value whenever it falls between the two limits, and locks to the nearest limit otherwise. Inside clamp() you can write math directly without wrapping it in calc(). **How do I calculate the preferred value for fluid typography?** Pick two target points, such as 16px at a 400px viewport and 20px at 1200px. The slope is the size difference divided by the viewport difference: 4 / 800 = 0.005, or 0.5vw. The intercept is the small size minus slope times the small viewport: 16 - 2 = 14px, or 0.875rem. The result is clamp(1rem, 0.875rem + 0.5vw, 1.25rem), which passes exactly through both targets. **Why should clamp() mix rem and vw instead of using vw alone?** A pure vw font size ignores the user's browser font-size preference and may not respond to text-only zoom, which is an accessibility problem. Adding a rem term to the preferred value keeps user preferences in play, and rem-based minimum and maximum values guarantee sensible bounds. It also gives you precise control: the rem + vw form defines an exact line through two chosen sizes rather than an eyeballed ratio. **What is the difference between min(), max(), and clamp()?** min() returns the smaller of its arguments, so it acts as a cap — min(90%, 60rem) never exceeds 60rem. max() returns the larger, so it enforces a floor. clamp() combines both: it bounds a fluid middle value on both sides. The naming feels backwards at first — min() sets a maximum and max() sets a minimum — which is why clamp() is usually the clearest choice for typography. **Does clamp() replace media queries entirely?** No. clamp() removes the breakpoints you used purely to resize text and spacing, but media queries still handle structural changes: a sidebar collapsing, navigation becoming a hamburger menu, or preference-based queries like prefers-reduced-motion and prefers-color-scheme. Most modern codebases pair fluid clamp() values for typography with a small number of media or container queries reserved for genuine layout shifts. **Is CSS clamp() safe to use in production?** Yes. clamp(), along with min() and max(), landed in Chrome, Edge, Firefox, and Safari during the first half of 2020, so every modern browser supports it. Unless you must support very old browsers, you can use it freely for font sizes, padding, margins, widths, and most other length-accepting properties. If you need a fallback, declare a static font-size first and let clamp() override it in supporting browsers. --- # CSS Container Queries: Style Components by Their Own Size > CSS container queries style an element based on the size of its parent container rather than the viewport. You declare a container with container-type: inline-size on a wrapper element, then write @container rules — for example @container (min-width: 400px) — that apply styles to the component inside whenever the container crosses that width. This lets one reusable component adapt automatically to a sidebar, a grid column, or a full-width slot, and it works in all modern browsers. Source: https://thesassway.com/css-container-queries/ Publisher: The Sass Way Published: 2026-08-06 Updated: 2026-08-06 ## What Are CSS Container Queries? CSS container queries let you style an element based on the size of its **parent container** instead of the size of the browser viewport. You mark an ancestor element as a container with the `container-type` property, then write `@container` rules that apply styles whenever that container is wider (or narrower) than a threshold you choose. That one shift solves a problem media queries never could: a component's layout can now respond to the space it's actually given. Drop the same card component into a narrow sidebar and a wide main column on the same page, and it can render as a compact stacked card in one spot and a roomy horizontal card in the other — no extra classes, no JavaScript, no duplicate CSS. Container queries are supported in all modern browsers and have been safe for production use since early 2023. If you build reusable components — and in 2026, almost everyone does — they belong in your toolkit next to [media queries](/css-media-queries/), not as a someday feature. ## Container Queries vs Media Queries Media queries ask, "How big is the viewport?" Container queries ask, "How big is the box this component lives in?" Both are essential; they just operate at different levels of your design. | | Media queries | Container queries | |---|---|---| | **Responds to** | Viewport (or device features) | A designated ancestor element | | **Best for** | Page-level layout: grids, navigation, column counts | Component-level layout: cards, widgets, form rows | | **Setup required** | None — works anywhere | An ancestor needs `container-type` | | **Same component, two contexts** | Needs modifier classes or duplicated rules | Adapts automatically to each context | | **Query units** | `vw`, `vh`, and friends | `cqw`, `cqh`, `cqi`, `cqb`, `cqmin`, `cqmax` | | **Syntax** | `@media (min-width: 768px)` | `@container (min-width: 400px)` | A practical rule of thumb: use media queries to decide **how many columns the page has**, and container queries to decide **how each component behaves inside its column**. The page-level thinking is covered in depth in our [responsive design guide](/responsive-design-guide/); this article handles the component level. ## Step 1: Create a Container with container-type Nothing responds to a container query until you declare a container. You do that on the element whose size you want to measure — typically a wrapper around your component, not the component itself. ```css .card-wrapper { container-type: inline-size; } ``` `container-type` accepts three values: - **`inline-size`** — the container can be queried by its inline dimension (width, in horizontal writing modes). This is the value you'll use 95% of the time. - **`size`** — the container can be queried by both width and height. Use sparingly: it requires the element's height to be determined without looking at its contents, which breaks the "height grows with content" behavior most layouts rely on. - **`normal`** — the default. The element isn't a size query container (though it can still be queried by style queries). ### Why inline-size is the safe default Declaring a query container applies **containment** to the element. With `container-type: inline-size`, the element's width can no longer be sized by its contents — it must come from its context, like a grid track, a flex basis, or a plain block-level width. In practice that's how most wrappers already work, which is why `inline-size` rarely causes surprises. Full `size` containment also locks down the height, which is why it's the riskier choice. ### Naming containers When containers nest — a card inside a sidebar inside a page — a `@container` rule resolves against the **nearest** ancestor container by default. To target a specific one, give it a name: ```css .sidebar { container-name: sidebar; container-type: inline-size; } ``` Or use the shorthand, which takes the name first, then the type: ```css .sidebar { container: sidebar / inline-size; } ``` ## Step 2: Write @container Rules With a container declared, `@container` works much like `@media`: a condition, then a block of normal rules. ```css .card { display: grid; grid-template-columns: 1fr; gap: 1rem; } @container (min-width: 400px) { .card { grid-template-columns: 120px 1fr; align-items: center; } } ``` Here the card stacks by default and switches to an image-beside-text layout whenever its container is at least 400px wide — regardless of what the viewport is doing. Note that this is the same mobile-first pattern you already use with media queries: start with the narrow layout, then enhance upward. If that workflow is new to you, our [mobile-first CSS](/mobile-first-css/) guide explains why it produces simpler stylesheets. To query a named container, put the name before the condition: ```css @container sidebar (min-width: 300px) { .widget { display: flex; } } ``` Modern range syntax works too, and reads more naturally: ```css @container (width > 400px) { .card { grid-template-columns: 120px 1fr; } } ``` One rule to remember: **a container query can't style the container it queries.** The styles inside `@container` apply to descendants of the container. If you need to restyle the wrapper itself, query an ancestor container above it. ## Container Query Units: cqw, cqh, and Friends Container queries ship with their own length units, mirroring viewport units but measured against the queried container: - **`cqw`** — 1% of the container's width - **`cqh`** — 1% of the container's height - **`cqi`** — 1% of the container's inline size (equals `cqw` in horizontal writing modes) - **`cqb`** — 1% of the container's block size - **`cqmin`** / **`cqmax`** — 1% of the smaller / larger of the two dimensions Their killer use case is fluid typography that scales with the component, not the page: ```css .card h2 { font-size: clamp(1.125rem, 4cqi + 0.5rem, 1.75rem); } ``` Now the same heading is modest in a cramped sidebar and generous in a wide feature slot, with no breakpoints at all. Prefer `cqi` over `cqw` when you can — it respects vertical writing modes for free. You can use these units anywhere a length is valid, even outside `@container` blocks. If no ancestor container exists, they fall back to behaving like the corresponding small viewport units, so they degrade sensibly. For a refresher on how all the CSS length units relate, see our [guide to CSS units](/css-units-guide/). ## Using Container Queries with Sass Sass passes `@container` through to the compiled CSS untouched, and nesting works exactly as it does with `@media`. That means you can keep component variants next to the component: ```scss @mixin container-up($min) { @container (min-width: #{$min}) { @content; } } .card { display: grid; gap: 1rem; @include container-up(400px) { grid-template-columns: 120px 1fr; } } ``` Dart Sass compiles that to: ```css .card { display: grid; gap: 1rem; } @container (min-width: 400px) { .card { grid-template-columns: 120px 1fr; } } ``` Two Sass-specific notes: - **Use interpolation in the condition.** Inside a `@container` rule's condition, wrap Sass variables as `#{$min}` so the compiled output contains the literal value. - **Centralize breakpoints in a map.** Just as teams keep viewport breakpoints in a Sass map, component breakpoints (`"card-horizontal": 400px`, `"widget-wide": 300px`) belong in one place so designers and developers share a vocabulary. ## A Real-World Example: One Card, Three Contexts Here's the pattern that sells container queries to every team that tries them. Imagine a product card used in three places: a 300px sidebar, a three-column grid, and a full-width feature slot. ```css .product-slot { container-type: inline-size; } .product-card { display: grid; gap: 0.75rem; } /* Enough room: image moves beside the text */ @container (min-width: 380px) { .product-card { grid-template-columns: 140px 1fr; } } /* Lots of room: bigger image, roomier spacing */ @container (min-width: 640px) { .product-card { grid-template-columns: 220px 1fr; gap: 1.5rem; } } ``` Every place the card appears, its slot is the container. In the sidebar, neither query matches and the card stacks. In the grid, each column is around 400px, so the first query kicks in. In the feature slot, both match and the last one wins, exactly like the mobile-first cascade you already know. The card's CSS never mentions the sidebar, the grid, or the feature slot — which means next month's brand-new context works without touching this file. Compare that with the media-query version: you'd need to know every viewport width at which each slot changes size, encode those page-level details into the component's styles, and update them whenever the page layout changes. That coupling is exactly what container queries remove. ## Container Queries Browser Support Size container queries and container query units are supported in every current major browser: Chrome and Edge (from version 105), Safari (from 16), and Firefox (from 110). All of those releases shipped by early 2023, so unless your audience includes very old browsers, you can use container queries today without a build step or polyfill. Two related features are worth tracking separately: - **Style queries** (`@container style(--theme: dark)`) let components respond to a container's custom property values rather than its size. Support arrived later and rolled out browser by browser, so check current support tables before relying on them in production. - **Progressive enhancement** is straightforward when you need it: write your stacked, narrow-friendly layout as the default, and put enhancements inside `@container`. Browsers that don't understand the rule simply keep the default layout — the same graceful-degradation story media queries have always had. ## Common Mistakes to Avoid - **Forgetting `container-type`.** An `@container` rule with no ancestor container never matches. If your query silently does nothing, this is almost always why. - **Making the component its own container.** Declare the container on a wrapper. If the element you're styling is the container, the query can't affect it. - **Reaching for `container-type: size` by habit.** Height-based queries force height containment, which collapses elements whose height should come from their content. Start with `inline-size`. - **Skipping names in nested layouts.** Unnamed queries resolve to the nearest container, which may not be the one you meant once components nest. Names cost nothing and prevent head-scratchers. - **Recreating page layout with container queries.** They complement media queries; they don't replace them. Viewport-level decisions still belong in `@media`. ## The Bottom Line Container queries close the gap that made truly reusable components impossible in CSS: components can finally own their responsive behavior. Declare a wrapper with `container-type: inline-size`, write `@container` rules the same mobile-first way you write media queries, and reach for `cqi` units when you want fluid sizing inside the component. Keep media queries for the page grid, hand everything inside it to container queries, and your components will work in every slot you'll ever drop them into — including the ones you haven't designed yet. ## Frequently asked questions **What is the difference between container queries and media queries?** Media queries respond to the browser viewport; container queries respond to the size of a designated ancestor element. Media queries suit page-level decisions like how many columns a layout has, while container queries suit component-level decisions like whether a card shows its image beside or above its text. They complement each other: use @media for the page grid and @container for the components placed inside it. **How do I enable container queries in CSS?** Add container-type: inline-size to the ancestor element you want to measure — typically a wrapper around your component, not the component itself. Then write an @container rule such as @container (min-width: 400px) { .card { display: flex; } }. The styles inside apply to descendants of the container whenever its width meets the condition. Without a declared container-type on an ancestor, @container rules never match. **What does container-type: inline-size do?** It makes the element a query container measured by its inline dimension — its width in horizontal writing modes — and applies inline-size containment, meaning the element's width must come from its context rather than its contents. It's the recommended value for almost all cases. The alternative, container-type: size, also allows height-based queries but locks down the element's height, which breaks layouts where height should grow with content. **What are container query units like cqw and cqi?** They are length units measured against the queried container instead of the viewport: cqw and cqh are 1% of the container's width and height, cqi and cqb are 1% of its inline and block size, and cqmin and cqmax take the smaller or larger dimension. They're ideal for fluid, component-scoped typography, such as font-size: clamp(1.125rem, 4cqi + 0.5rem, 1.75rem). Prefer cqi, which respects writing modes. **Are CSS container queries supported in all browsers?** Yes — size container queries and container query units work in every current major browser: Chrome and Edge from version 105, Safari from 16, and Firefox from 110, all released by early 2023. No polyfill or build step is needed for modern audiences. Style queries, which respond to custom property values instead of size, arrived later and rolled out gradually, so check current support tables before using them. **Can a container query style the container itself?** No. The styles inside an @container rule apply only to descendants of the container being queried, because letting a container restyle itself based on its own size could create infinite loops. If you need to change the wrapper's own styling, declare a container higher up the tree and query that ancestor instead. This is also why you should declare the container on a wrapper rather than on the component you're styling. --- # CSS Gradients: Linear, Radial, and Conic Explained > A CSS gradient is a browser-generated image that blends two or more color stops. Use linear-gradient() when colors should run along a line, radial-gradient() when they should spread from a point, and conic-gradient() when they should sweep around a point. Each has a repeating form. Set direction or geometry first, then stops; test contrast across the full result and verify newer interpolation syntax against your browser support matrix. Source: https://thesassway.com/css-gradients/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-05 ## CSS gradients in one minute A **CSS gradient** is a browser-generated image that blends two or more color stops. Use `linear-gradient()` for color along a straight line, `radial-gradient()` for color spreading from a point, and `conic-gradient()` for color sweeping around a point. Each function also has a repeating form. The [CSS Images specification](https://www.w3.org/TR/css-images-4/#gradients) defines the geometry; the stops define where each color is exact and how the browser fills the space between them. ```css .linear { background: linear-gradient(to right, #6a11cb, #2575fc); } .radial { background: radial-gradient(circle, #6a11cb, #2575fc); } .conic { background: conic-gradient(from 0deg, #6a11cb, #2575fc); } ``` A gradient is an ``, not a color. That distinction matters: it works in image-valued properties such as `background-image`, `border-image`, and `mask`, but not in `background-color`. It has no natural dimensions, so the property using it supplies the gradient box. MDN classifies the core gradient type as [widely available](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/gradient), including current Chrome, Edge, Firefox, and Safari releases. Test the exact syntax you use, especially newer color-interpolation options, against the browsers in your support matrix. ## How does linear-gradient() work? `linear-gradient()` creates a gradient line through the box. Its first optional argument sets the line direction; the remaining arguments are color stops. ```css .hero { background: linear-gradient(to bottom right, #ff512f, #dd2476); } ``` ### Keywords and angles are different controls - Keywords such as `to top`, `to right`, and `to bottom left` point toward a side or corner and adapt to the box proportions. - Angles such as `45deg`, `0.25turn`, and `3.14rad` set a fixed direction. `0deg` points up, angles increase clockwise, `90deg` points right, and `180deg` points down. On a square, `to top right` and `45deg` appear equivalent. On a wide rectangle they diverge: the keyword aims toward the corner, while the angle remains 45 degrees. This is the CSS gradient-line geometry sometimes called “magic corners.” ```css .keyword { background: linear-gradient(to top right, #000, #fff); } .angle { background: linear-gradient(45deg, #000, #fff); } ``` With no direction, a linear gradient runs from top to bottom. ### Color stops, hard edges, and hints A stop position says where a color must be exact. Omit positions and the browser distributes the stops automatically. Give one color two positions to make a solid band: ```css .stripes { background: linear-gradient( to right, #6a11cb 0% 50%, #2575fc 50% 100% ); } ``` The double-position form `#6a11cb 0% 50%` is shorthand for two stops of the same color. Adjacent colors meeting at 50% create a hard edge, useful for flags, split backgrounds, and progress bars. A bare position between colors is a transition hint, not another stop: ```css .biased-blend { background: linear-gradient(to right, #f00, 20%, #00f); } ``` Here the 50% blend between red and blue occurs at 20% of the gradient line. The transition remains smooth, but its balance shifts. The [color-stop algorithm](https://www.w3.org/TR/css-images-4/#color-stop-syntax) specifies both hard stops and hints. ## How does radial-gradient() work? `radial-gradient()` sends color outward from a center. Its prelude can set a shape, size, and position before the stop list: ```css .glow { background: radial-gradient( circle at center, #ffd200, #f7971e 70% ); } ``` - Shape is `circle` or `ellipse`; `ellipse` is the default. - Size may be explicit or use `closest-side`, `closest-corner`, `farthest-side`, or `farthest-corner`. The default ending shape is `farthest-corner`. - Position follows `at`, using values such as `center`, `top left`, or `30% 70%`. For a contained spotlight, set a radius and fade to transparent: ```css .spotlight { background: radial-gradient( circle 120px at 30% 40%, rgb(255 255 255 / 90%), rgb(255 255 255 / 0%) 100% ); } ``` The `120px` radius fixes the circle size. The transparent final stop prevents an abrupt visible boundary. ## How does conic-gradient() work? `conic-gradient()` measures stop positions around a center, like marks around a dial. That makes it useful for progress rings, loading indicators, color wheels, and pie-shaped decoration. ```css .pie { background: conic-gradient( #e63946 0% 25%, #f1faee 25% 50%, #a8dadc 50% 75%, #457b9d 75% 100% ); border-radius: 50%; } ``` The four sectors cover 0–25%, 25–50%, 50–75%, and 75–100%. `border-radius: 50%` makes the box circular. Change the starting angle and center with `from 90deg at 50% 50%`. Do not let colored sectors carry data by themselves. A conic-gradient chart is only paint: give the surrounding HTML an accessible label and present the values in text or a table too. Color alone must not be the only way a reader distinguishes categories under [WCAG 2.2 Success Criterion 1.4.1](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color). A hue wheel can use HSL stops: ```css .wheel { background: conic-gradient( hsl(0 100% 50%), hsl(60 100% 50%), hsl(120 100% 50%), hsl(180 100% 50%), hsl(240 100% 50%), hsl(300 100% 50%), hsl(360 100% 50%) ); border-radius: 50%; } ``` ## Which CSS gradient should you use? | Function | Colors travel | Useful for | Default geometry | |---|---|---|---| | `linear-gradient()` | Along a straight line | Backgrounds, buttons, overlays, stripes | Top to bottom | | `radial-gradient()` | Outward from a point | Spotlights, glows, vignettes, orbs | Centered ellipse to farthest corner | | `conic-gradient()` | Around a point | Progress rings, sectors, color wheels | From 0deg at center | Choose by geometry. If the design language is “across,” start linear. If it is “outward,” start radial. If it is “around,” start conic. ## How do you stack and repeat gradients? Separate background images with commas. The first image is painted closest to the user, so transparency reveals the layers below it: ```css .layered { background: linear-gradient(rgb(0 0 0 / 60%), rgb(0 0 0 / 60%)), radial-gradient(circle at 20% 20%, #ff8a00, transparent 40%), linear-gradient(120deg, #1a2980, #26d0ce); } ``` To darken a photo, put the translucent gradient before `url(...)`. It is still necessary to test text contrast over the complete rendered result; an overlay value such as 60% is not, by itself, proof of compliance. Repeating functions repeat the interval represented by the stop list: ```css .barber-pole { background: repeating-linear-gradient( 45deg, #ffcc00 0 20px, #222 20px 40px ); } ``` This pattern repeats every 40px. Two transparent repeating gradients set 90 degrees apart can form a grid. Repeating radial and conic gradients follow the same principle, but repeat along their radius or angle. ## How do color interpolation spaces change a gradient? Two gradients can use identical endpoints and still produce different midpoints. CSS Images Level 4 allows an interpolation method inside the function. For example: ```css .legacy-mix { background: linear-gradient(in srgb to right, #f01, #081); } .perceptual-mix { background: linear-gradient(in oklab to right, #f01, #081); } ``` The CSS Working Group’s [interpolation examples](https://www.w3.org/TR/css-images-4/#color-interpolation-example) show that sRGB can create a darker midpoint, while Lab or Oklab can produce a more perceptually uniform blend. Polar spaces such as Oklch preserve chroma differently and can take different hue paths. Treat this as a design decision, not a universal upgrade: compare the rendered midpoint, confirm browser support for the syntax, and provide a preceding fallback declaration if necessary. ```css .banner { background: linear-gradient(to right, #f01, #081); background: linear-gradient(in oklab to right, #f01, #081); } ``` A browser that rejects the second declaration retains the first. Write the standards syntax first and let your support policy drive any compatibility transforms. If the project already uses [Autoprefixer](https://github.com/postcss/autoprefixer), it adds or removes vendor prefixes from Can I Use data according to the project’s Browserslist targets; it does not polyfill unsupported gradient behavior. That is why the fallback declaration above still matters for newer syntax. ## How do you animate a CSS gradient reliably? Animating between complete gradient images is not a dependable cross-browser foundation. Animate a property around the gradient instead: move an oversized background, rotate a containing element, cross-fade a pseudo-element, or register a typed custom property used inside the gradient. The background-position pattern preserves the existing 600% canvas and 12-second timing: ```css .animated { background: linear-gradient(270deg, #ee7752, #e73c7e, #23a6d5); background-size: 600% 600%; animation: drift 12s ease infinite; } @keyframes drift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } ``` Our [CSS animations tutorial](/css-animations-tutorial/) covers timing and keyframes. Respect reduced-motion preferences when movement is decorative: ```css @media (prefers-reduced-motion: reduce) { .animated { animation: none; } } ``` For an angle, register the custom property so the browser knows that its values interpolate as angles. The [CSS Properties and Values API](https://www.w3.org/TR/css-properties-values-api-1/#animation-behavior) defines typed custom-property interpolation, and MDN marks the `@property` at-rule as [Baseline 2024](https://developer.mozilla.org/en-US/docs/Web/CSS/%40property). ```css @property --sweep-angle { syntax: ""; inherits: false; initial-value: 0deg; } .spinner { background: conic-gradient(from var(--sweep-angle), #2575fc, #6a11cb); animation: sweep 2s linear infinite; } @keyframes sweep { to { --sweep-angle: 360deg; } } ``` For general theming, ordinary [CSS custom properties](/css-variables-guide/) keep stops in one place: ```css :root { --grad-start: #6a11cb; --grad-end: #2575fc; } .card { background: linear-gradient(135deg, var(--grad-start), var(--grad-end)); } ``` ## Which background box does a gradient fill? The short answer needs two properties. `background-origin` sets the positioning area and starts at `padding-box` by default. `background-clip` sets the painting boundary and starts at `border-box`, so a background is normally painted underneath a border. The [CSS Backgrounds specification](https://www.w3.org/TR/css-backgrounds-3/#backgrounds) defines those defaults separately. ```css .contained { border: 8px solid rgb(0 0 0 / 25%); padding: 24px; background-image: linear-gradient(to right, #6a11cb, #2575fc); background-origin: content-box; background-clip: content-box; } ``` This example both positions and clips the gradient to the content box. If a result appears shifted, cropped, or visible under a translucent border, inspect both properties along with `background-size`, not just `background-clip`. Our [CSS box model guide](/css-box-model/) covers the content, padding, and border boxes behind those values. ## What accessibility and rendering checks matter? - **Test contrast everywhere text can land.** WCAG 2.2 requires at least 4.5:1 for normal text and 3:1 for large text. With a gradient, test the lowest-contrast point behind each line, not an average color. The exact thresholds are in [Success Criterion 1.4.3](https://www.w3.org/TR/WCAG22/#contrast-minimum). - **Do not encode meaning only by color.** Add text, icons, patterns, or labels when stops distinguish status or data. - **Offer a motion-reduced state.** A static gradient is harmless; an endlessly drifting or rotating one is motion. - **Expect banding.** Long, subtle blends can reveal bands on 8-bit displays. An intermediate stop may redistribute the blend, but it cannot guarantee that every display or screenshot pipeline will avoid banding. - **Measure animated effects.** A generated gradient avoids an image download, but large animated backgrounds still require repainting. Profile the actual page instead of assuming that “no request” means “no rendering cost.” For debugging, reduce a failing declaration to two named colors, remove layers, and restore one argument at a time. Check commas first, then direction or shape syntax, stop order, unsupported interpolation syntax, and finally the background sizing and box properties. That sequence usually separates a parsing error from geometry that is valid but visually surprising. ## Frequently asked questions **How do you make a gradient in CSS?** Assign a gradient function to an image-valued property: background: linear-gradient(to right, #6a11cb, #2575fc);. The optional first argument controls direction or geometry, followed by at least two color stops. Use radial-gradient() for an outward blend or conic-gradient() for a sweep around a center. **What is the difference between linear, radial, and conic gradients?** A linear gradient blends along a straight line, a radial gradient spreads outward from a center in a circle or ellipse, and a conic gradient sweeps colors around a center like a dial. Choose the function by the geometry of the effect, then control the exact result with positions and color stops. **How do you set the direction of a linear gradient?** Put a keyword or angle before the color stops. Keywords such as 'to right' and 'to top left' aim at a side or corner and adapt to the box. Angles are fixed: 0deg points up, 90deg points right, and 180deg points down. A corner keyword and 45deg can differ on a non-square box. **Can you animate a CSS gradient?** Use a dependable surrounding control: animate background-position on an oversized gradient, rotate a wrapper, cross-fade a pseudo-element, or register a typed custom property used as an angle or stop. Add a prefers-reduced-motion rule for decorative motion, and test @property support in the browsers you target. **How do you put a gradient over an image?** List a translucent gradient before the image URL because the first background layer is painted on top: background: linear-gradient(rgb(0 0 0 / 60%), rgb(0 0 0 / 60%)), url(photo.jpg);. Adjust the overlay, then test the lowest text-to-background contrast across the complete rendered image. **Do CSS gradients need vendor prefixes?** The core CSS gradient type is widely available without hand-written prefixes. Newer additions, including explicit color-interpolation syntax, may have a different support range. Check the exact syntax against your browser matrix and place an older, valid gradient declaration immediately before an enhanced declaration when you need progressive enhancement. --- # CSS Grid Tutorial: Build a Responsive Layout Step by Step > CSS Grid lays out rows and columns together. Start with `display: grid`, define tracks with `grid-template-columns`, and use `gap` for gutters. For responsive cards, `repeat(auto-fit, minmax(250px, 1fr))` creates as many 250px-minimum tracks as the container can hold. Then add line placement or named areas only where the design needs them, and test overflow, source order, and feature support with real content. Source: https://thesassway.com/css-grid-tutorial/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-07 ## Build a Responsive Grid Layout in Under 20 Lines of CSS CSS Grid is a two-dimensional layout system: it controls rows **and** columns at the same time, which is exactly what page layouts need. Here is a complete, responsive grid you can paste into any project right now: ```css .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } ``` That is the whole trick for a card layout. Every direct child of `.grid` becomes a grid item and flows into columns that are at least `250px` wide, growing to fill the space and wrapping to new rows automatically — no media queries required. The rest of this tutorial explains *why* each line does what it does, then builds a full page layout on top of it. [MDN's CSS Grid guide](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Basic_concepts) marks core Grid as widely available across browsers since October 2017. That covers current Chrome, Firefox, Safari, and Edge releases, but production support still depends on the exact feature you use. Check the feature, not only the words "CSS Grid": subgrid, for example, arrived later than the original grid container and track features. ## Step 1: Turn an Element Into a Grid Grid starts with one declaration on the parent, called the **grid container**. Its direct children become **grid items** automatically. ```css .container { display: grid; } ``` By itself, `display: grid` stacks items in a single column. The layout begins once you define tracks — the columns and rows. ### The two properties that define tracks ```css .container { display: grid; grid-template-columns: 200px 200px 200px; grid-template-rows: 100px 100px; } ``` This creates a fixed 3-column, 2-row grid. `grid-template-columns` lists the width of each column left to right; `grid-template-rows` lists the height of each row top to bottom. The number of values equals the number of tracks. ## Step 2: Use the `fr` Unit Instead of Fixed Widths Fixed pixel widths break on small screens. The **`fr` unit** ("fraction") solves this by splitting the *available* space into shares. ```css .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; } ``` Here `1fr 1fr 1fr` means "three equal columns." Change the ratio and the split changes with it: `2fr 1fr` gives a wide column beside a narrow one, always at a 2:1 ratio no matter the viewport width. `fr` also plays well with fixed values. A common sidebar layout is one fixed rail and one flexible main area: ```css .layout { display: grid; grid-template-columns: 250px 1fr; gap: 2rem; } ``` The sidebar stays `250px`; the main column absorbs whatever remains. ### `repeat()` keeps long track lists readable Typing `1fr 1fr 1fr 1fr` gets old fast. `repeat()` is shorthand: ```css grid-template-columns: repeat(4, 1fr); /* four equal columns */ grid-template-columns: repeat(2, 200px 1fr); /* 200px 1fr 200px 1fr */ ``` ## Step 3: Add Gaps Instead of Margins Spacing between grid items uses the `gap` property, not margins. Margins on grid items create uneven, hard-to-manage gutters; `gap` applies space *between* tracks only, never on the outer edge. ```css .container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; /* row and column gap */ /* or set them separately: */ row-gap: 1rem; column-gap: 2rem; } ``` `gap` is one of Grid's best features — you get clean, consistent gutters with a single line, and it works in Flexbox too. ## Step 4: Make It Responsive Without Media Queries This is where Grid outshines older techniques. The combination of `auto-fit`, `minmax()`, and `repeat()` builds a layout that reflows on its own. ```css .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } ``` Read it inside-out: - **`minmax(250px, 1fr)`** — each column is never narrower than `250px` and never wider than an equal share of free space. - **`auto-fit`** — fit as many `250px`+ columns as the container allows, then stretch them to fill the row. - **`repeat()`** — apply that rule to every column it can create. Suppose the grid's content box is exactly `1200px` wide and `1rem` resolves to `16px`. Four `250px` columns plus three gaps need `1048px`, so four columns fit. Five columns plus four gaps need `1314px`, so five do not. The remaining `152px` is divided by the four `1fr` maxima, producing four `288px` columns. That arithmetic is why "four or five columns on a 1200px screen" is not a reliable promise. Viewport width is not necessarily grid width, `rem` can change, padding and borders consume space, and scrollbar space may matter. Inspect the grid container's content box in developer tools when a breakpoint surprises you. ### `auto-fit` vs `auto-fill` These two keywords look identical but differ when items are few: | Keyword | Behavior when items don't fill the row | | --- | --- | | `auto-fit` | Empty tracks collapse; existing items stretch to fill the row | | `auto-fill` | Empty tracks are kept at their minimum width; items stay their size | Use **`auto-fit`** when you want items to grow and fill the space (most card layouts). Use **`auto-fill`** when you want a fixed item size and are happy leaving trailing empty space. If you only ever have enough items to fill a row, the two behave the same. Once your layout is fluid, you can still add [media queries](/css-media-queries/) to fine-tune spacing or swap the whole structure at specific breakpoints. ## Step 5: Place Items Precisely With Line Numbers Grid numbers its lines starting at `1` on the left/top. You position an item by telling it which lines to start and end on. ```css .featured { grid-column: 1 / 3; /* span from column line 1 to line 3 = 2 columns */ grid-row: 1 / 2; } ``` The `span` keyword is often clearer than counting lines: ```css .featured { grid-column: span 2; /* take up 2 columns, wherever it lands */ grid-row: span 2; } ``` This is how you build a "featured" tile that is twice as wide as its neighbors, or a hero cell that dominates a masonry-style grid — no absolute positioning, no float hacks. ## Step 6: Name Regions With `grid-template-areas` For full-page layouts, line numbers get abstract. **Named template areas** let you draw the layout in your CSS as ASCII art, which is far easier to read and maintain. ```css .page { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header" "sidebar main" "footer footer"; gap: 1rem; min-height: 100vh; } .page > header { grid-area: header; } .page > nav { grid-area: sidebar; } .page > main { grid-area: main; } .page > footer { grid-area: footer; } ``` Each quoted string is a row; each word is a column cell. Repeating a name (like `header`) spans that region across cells. This is the classic "holy grail" layout — header, footer, sidebar, and content — in a form you can read at a glance and rearrange for mobile in a media query. To collapse it to a single column on small screens, redraw the map: ```css @media (max-width: 600px) { .page { grid-template-columns: 1fr; grid-template-areas: "header" "main" "sidebar" "footer"; } } ``` ## Step 7: Align Nested Cards With `subgrid` A nested `display: grid` normally creates an independent set of tracks. Its columns and rows do not automatically line up with the parent. The `subgrid` value lets a nested grid use the parent grid's track definition instead. [MDN's subgrid guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid) demonstrates that relationship, and CSS Grid Level 2 adds subgrid specifically so nested grids can participate in parent sizing. Here is a card row whose headings, body copy, and actions align across cards: ```css .cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; } .card { display: grid; grid-template-rows: subgrid; grid-row: span 3; } ``` Each `.card` spans three parent rows, then adopts those rows through `subgrid`. Use this when nested content needs shared alignment. Use an ordinary nested grid when the component should size itself independently. Because subgrid support arrived later than core Grid, verify the browsers in your project's support policy before shipping it. ## Aligning Items Inside the Grid Grid has two axes, so it has alignment properties for each. These control how items sit **inside** their cells and how tracks sit **inside** the container. | Property | Applies to | Axis it controls | | --- | --- | --- | | `justify-items` | items within their cell | inline (row / horizontal) | | `align-items` | items within their cell | block (column / vertical) | | `justify-content` | the whole grid within the container | inline | | `align-content` | the whole grid within the container | block | | `place-items` | shorthand for `align-items` + `justify-items` | both | Centering a single item inside its cell is a one-liner: ```css .cell { display: grid; place-items: center; } ``` `place-items: center` is one of the cleanest ways to center content both horizontally and vertically. It is worth comparing against the other centering techniques and the alignment model in [Flexbox](/flexbox-guide/), which shares the same `justify`/`align` vocabulary. ## Implicit vs Explicit Grids The tracks you define with `grid-template-columns` and `grid-template-rows` form the **explicit grid**. When you add more items than fit, Grid creates **implicit** tracks to hold them. Control their size with `grid-auto-rows`: ```css .gallery { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 200px; /* every new row is 200px tall */ gap: 1rem; } ``` Without `grid-auto-rows`, implicit rows size to their content, which is often what you want for text but not for uniform image tiles. ## A Complete Responsive Example Here is everything combined — a page shell that is fluid by default and collapses cleanly on mobile: ```css .site { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "header" "hero" "cards" "footer"; gap: 2rem; max-width: 1200px; margin-inline: auto; padding: 1rem; } .site > header { grid-area: header; } .site > .hero { grid-area: hero; } .site > footer { grid-area: footer; } .cards { grid-area: cards; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; } ``` The outer grid stacks the page sections; the inner `.cards` grid handles the responsive card row. Two grids, no media queries, and it works from a 320px phone to a wide desktop. > `minmax(0, 1fr)` instead of plain `1fr` prevents a common overflow bug where long content or images refuse to shrink below their intrinsic size. Reach for it whenever a grid item overflows its track. ## Why `1fr` Can Still Overflow An `fr` track is flexible, but a grid item can contribute an automatic minimum size based on its content. A long unbroken URL, a preformatted code block, or an image with an intrinsic width can therefore prevent a plain `1fr` column from shrinking as far as you expected. The Grid specification documents this [automatic minimum size](https://drafts.csswg.org/css-grid/#min-size-auto). Fix the track when the track should be allowed to shrink: ```css .layout { grid-template-columns: minmax(0, 1fr) 18rem; } ``` Fix the item when that item is the constraint: ```css .main { min-width: 0; } .main img { max-width: 100%; } ``` These remedies solve different problems. `minmax(0, 1fr)` changes the track minimum; `min-width: 0` permits a particular item to shrink. Neither wraps an unbreakable token by itself, so text may also need `overflow-wrap: anywhere` and code may need intentional overflow. ## CSS Grid vs Flexbox: Which to Reach For They are not rivals — most real interfaces use both. The short version: | Use Grid when… | Use Flexbox when… | | --- | --- | | You need rows **and** columns aligned together | You need items in a single row or column | | You're building the overall page structure | You're spacing items inside one component | | Cells should line up across both axes | Content length should drive the sizing | | You want named layout regions | You want simple wrapping or distribution | A reliable rule of thumb: **Grid for the layout, Flexbox for the content inside each region.** For a deeper decision guide with side-by-side code, see [Flexbox vs CSS Grid](/flexbox-vs-grid/). ## Keep Visual Order and Reading Order Aligned Grid placement can move an item visually without moving it in the document. Keyboard focus, screen-reader reading order, and copy order still follow the source. Do not use `grid-row`, `grid-column`, or the `order` property to turn illogical HTML into a visually tidy interface. Write the HTML in a meaningful order first, then use Grid for layout. If a mobile design needs a fundamentally different sequence, test keyboard navigation and assistive-technology reading order rather than judging only the screenshot. ## Common Grid Mistakes to Avoid - **Using margins for gutters.** Use `gap`. It never doubles up or leaks onto outer edges. - **Forgetting `minmax(0, 1fr)`.** Plain `1fr` won't shrink below content size, causing overflow with long words or images. - **Setting widths on grid items.** Let the tracks (`grid-template-columns`) define sizing; item widths fight the grid. - **Reaching for media queries too early.** `auto-fit` + `minmax()` handles most responsive cases on its own. - **Confusing `auto-fit` and `auto-fill`.** They only differ when items are sparse — `auto-fit` collapses empty tracks, `auto-fill` keeps them. ## Where to Go Next You now have every core Grid concept: containers, `fr` units, `repeat()`, `gap`, responsive `auto-fit` grids, line-based placement, and named areas. The fastest way to internalize them is to rebuild a layout you already know — a dashboard, a blog index, a pricing page — using nothing but Grid. From here, pair Grid with the alignment and sizing patterns in the full [Flexbox guide](/flexbox-guide/), and layer in breakpoints with [CSS media queries](/css-media-queries/) when you need structural changes rather than fluid reflow. Master those three tools together and there is very little on the web you cannot lay out. ## References and testing note The feature explanations above were rechecked against [MDN's Grid basics](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Basic_concepts), [MDN's `repeat()` reference](https://developer.mozilla.org/en-US/docs/Web/CSS/repeat), [MDN's subgrid guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid), and the CSS Working Group's [Grid Level 2 draft](https://drafts.csswg.org/css-grid/). *Code examples are provided as-is for education. Test them with your actual content, browser-support policy, writing modes, zoom settings, keyboard navigation, and assistive-technology workflow before production.* ## Frequently asked questions **How do I make a CSS Grid responsive without media queries?** Use `grid-template-columns: repeat(auto-fit, minmax(250px, 1fr))` with `gap`. The minimum sets the narrowest track, `auto-fit` creates as many tracks as the container can hold and collapses empty ones, and `1fr` shares remaining space. Media queries are still useful when the layout itself, rather than only track count, must change. **How many 250px Grid columns fit in 1200px?** With a 1200px content box and a 16px gap, four columns fit: 4 x 250px plus 3 x 16px equals 1048px. Five need 1314px. With `minmax(250px, 1fr)`, the remaining 152px is shared, producing four 288px columns. Padding, borders, scrollbar space, and a different root font size change the result. **What is the difference between auto-fit and auto-fill?** Both repeat tracks to fill available space. After items are placed, `auto-fit` collapses empty repeated tracks, allowing occupied tracks to stretch. `auto-fill` retains those empty tracks. When enough items occupy every track, the results can look identical. Use developer tools with a short item list to see the difference. **Why does 1fr overflow a CSS Grid?** A grid item can contribute an automatic minimum size based on its content, so a long URL, code block, or intrinsically wide image can keep a `1fr` track from shrinking. Use `minmax(0, 1fr)` when the track may shrink, `min-width: 0` on the constrained item, and an appropriate text or media overflow rule. **What does subgrid do?** A normal nested grid creates independent tracks. `subgrid` lets the nested grid use the relevant parent track definition, which is useful when headings, descriptions, and actions must align across cards. The child must span parent tracks before adopting them. Check the exact feature against your project's browser-support policy because subgrid arrived after core Grid. **Should I use CSS Grid or Flexbox?** Use Grid when relationships across rows and columns drive the layout. Use Flexbox when distribution along one main axis drives a component. Grid for page layout and Flexbox inside components is a useful starting heuristic, not a rule; many components use Grid, and many pages use Flexbox. --- # CSS Media Queries: How to Make Your Site Responsive > A CSS media query applies styles only when the browser meets a condition, usually viewport width. You wrap CSS in an @media block, like @media (min-width: 768px) { }, and those rules run only when the condition is true. This lets one stylesheet adapt to phones, tablets, and desktops. Write mobile-first with min-width, and always include the viewport meta tag. Source: https://thesassway.com/css-media-queries/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## What a CSS Media Query Actually Does A **media query** is a CSS rule that applies styles only when the browser meets a condition you specify, most often the width of the viewport. You wrap normal CSS in an `@media` block, and the browser runs those declarations only when the condition is true. That single mechanism is what lets one stylesheet serve a 360px phone and a 1440px monitor without shipping two separate sites. Here is the whole idea in four lines: ```css .card { padding: 1rem; } @media (min-width: 768px) { .card { padding: 2rem; } } ``` On narrow screens the card has `1rem` of padding. Once the viewport reaches `768px`, the second rule wins and padding becomes `2rem`. Nothing else changes. Media queries never *replace* your CSS; they layer conditional overrides on top of it. ## The Syntax, Piece by Piece Every media query has up to three parts: a **media type**, one or more **media features** in parentheses, and the styles inside the braces. ```css @media screen and (min-width: 600px) and (max-width: 1199px) { /* styles here apply only in this range */ } ``` - **`screen`** is the media type. The others you will actually use are `print` (for print stylesheets) and `all` (the default). The old `handheld`, `tv`, and `aural` types are deprecated, so ignore them. - **`(min-width: 600px)`** is a media feature. Features test the environment: width, height, orientation, resolution, color scheme, and more. - **`and`** combines conditions. All of them must be true for the block to apply. If you omit the media type, the default is `all`, so `@media (min-width: 600px)` is exactly equivalent to `@media all and (min-width: 600px)`. That means it also applies when printing, not just on screen. This bare-feature form is the common shorthand precisely because it targets every medium. Add an explicit type only when you specifically mean to target `print`, or to scope styles to `screen` and keep them out of print. ### Combining Conditions: and, comma, not | Operator | Meaning | Example | |----------|---------|---------| | `and` | All conditions must match | `(min-width: 768px) and (orientation: landscape)` | | `,` (comma) | Either condition matches (OR) | `(max-width: 600px), (orientation: portrait)` | | `not` | Negate the whole query | `not all and (min-width: 768px)` | The comma is an OR: the styles apply if *any* comma-separated query is true. Mixing them lets you express fairly precise rules without JavaScript. ## min-width vs max-width: The Decision That Shapes Your CSS This is the question most developers get stuck on, so answer it first and the rest of your responsive CSS falls into place. - **`min-width`** means "this width **or wider**." Styles cascade *upward*. This is the **mobile-first** approach: you write base styles for small screens, then add `min-width` queries that enhance the layout as the screen grows. - **`max-width`** means "this width **or narrower**." Styles cascade *downward*. This is the **desktop-first** approach: you design for large screens, then use `max-width` queries to walk the layout back down for phones. These are two independent approaches to the same layout. They use different class names (`.grid-mf` and `.grid-df`) so you can drop both into one stylesheet to compare them without the base rules colliding. In a real project you would pick one approach and use a single `.grid` class. ```css /* Mobile-first: base styles are the phone layout */ .grid-mf { display: block; } @media (min-width: 768px) { .grid-mf { display: grid; grid-template-columns: repeat(2, 1fr); } } ``` ```css /* Desktop-first: base styles are the desktop layout */ .grid-df { display: grid; grid-template-columns: repeat(2, 1fr); } @media (max-width: 767px) { .grid-df { display: block; } } ``` Both produce the same result here, but the mental models are different. **Mobile-first is the modern default** because base styles stay simple, you ship less override code, and the smallest devices (often the slowest) get the leanest CSS. If you want the full argument for why, see our guide on [mobile-first CSS](/mobile-first-css/). ### Watch the off-by-one gap A classic bug: pairing `max-width: 768px` with `min-width: 768px`. At *exactly* `768px` both rules can match, causing overlap. Keep ranges from colliding by using `max-width: 767.98px`, or better, avoid `max-width` in a mobile-first codebase entirely so you never need paired boundaries. ## Choosing Breakpoints A **breakpoint** is the viewport width where your layout changes. The single biggest mistake is picking breakpoints to match specific phones. Devices change every year; your content does not. **Set breakpoints where the design breaks, not where a device sits.** Widen your browser slowly and watch. The moment a line of text gets too long, a card gets awkwardly wide, or whitespace balloons, that width is a breakpoint. This gives you a layout that looks intentional on hardware that does not exist yet. That said, a conventional starting set is useful as a reference. These roughly follow the values popular frameworks converged on: | Breakpoint | Common `min-width` | Typical target | |------------|-------------------|----------------| | Small | `640px` | Large phones, small tablets portrait | | Medium | `768px` | Tablets | | Large | `1024px` | Small laptops, tablets landscape | | Extra large | `1280px` | Desktops | | 2XL | `1536px` | Large monitors | Use these as a skeleton, then add or drop breakpoints based on what your actual content needs. Two well-placed breakpoints often beat five arbitrary ones. ### Use rem, not px, for query units Breakpoints written in `px` ignore the user's browser font-size preference. Written in `rem`, they scale with it, which is better for accessibility. `768px` becomes `48rem` (768 ÷ 16). Note that `em`/`rem` in media queries are always relative to the browser default (usually 16px), *not* the element's font size. If the difference between `px`, `em`, and `rem` is fuzzy, our [CSS units guide](/css-units-guide/) untangles it. ```css @media (min-width: 48rem) { /* 768px at the default font size, but respects user zoom */ } ``` ## Modern Range Syntax Older media queries lean on `min-` and `max-` prefixes. Modern browsers support cleaner comparison operators that read like math: ```css /* Old way */ @media (min-width: 768px) and (max-width: 1199px) { } /* New range syntax */ @media (768px <= width <= 1199px) { } /* Single bound */ @media (width >= 768px) { } ``` The range syntax also sidesteps the off-by-one boundary problem, since `<` and `<=` are explicit. It is supported in all current versions of Chrome, Edge, Firefox, and Safari (Safari gained it in 16.4, early 2023). If you must support older browsers, keep the `min-width`/`max-width` form or provide it as a fallback. ## Media Features Beyond Width Width gets the attention, but several other features are genuinely useful in production. ### prefers-color-scheme Respect the user's system dark-mode setting with zero JavaScript: ```css :root { --bg: #ffffff; --text: #1a1a1a; } @media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; --text: #f5f5f5; } } ``` ### prefers-reduced-motion Users who get motion sickness can ask the OS to reduce animation. Honor it: ```css @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } ``` ### orientation and others | Feature | What it tests | Example value | |---------|---------------|---------------| | `orientation` | Portrait vs landscape | `landscape` | | `aspect-ratio` | Viewport width-to-height ratio | `16/9` | | `resolution` | Pixel density | `2dppx` (retina) | | `hover` | Whether the device can hover | `hover` / `none` | | `pointer` | Precision of the pointer | `fine` / `coarse` | `hover` and `pointer` are quietly powerful: they let you distinguish a mouse from a touchscreen and, for example, only show hover effects on devices that can actually hover. ## The One Line That Makes It All Work None of this responds correctly on real phones without the **viewport meta tag** in your HTML ``: ```html ``` Without it, mobile browsers pretend to be about `980px` wide and zoom out, so your `min-width` queries fire against the wrong number and the page looks like a shrunken desktop. This single tag is the most common reason "my media queries aren't working." Add it to every page. ## A Complete, Practical Example Here is a mobile-first card grid that adapts across three breakpoints, uses `rem` units, and respects dark mode: ```css .cards { display: grid; gap: 1rem; grid-template-columns: 1fr; padding: 1rem; } @media (min-width: 40rem) { .cards { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 64rem) { .cards { grid-template-columns: repeat(3, 1fr); gap: 1.5rem; padding: 2rem; } } ``` One column on phones, two on tablets, three on desktops, all from a single stylesheet with no JavaScript. That is the entire promise of responsive design delivered by three small blocks. ## When Media Queries Aren't the Right Tool Media queries respond to the **viewport**, not to the space a component actually occupies. A sidebar card and a full-width card have the same viewport but very different available widths. For component-level responsiveness, **container queries** (`@container`) are the better tool, and they are now supported in all major browsers. Media queries remain the right choice for page-level layout, global spacing, dark mode, and motion preferences. Both fit into the bigger picture covered in our [responsive design guide](/responsive-design-guide/). ## Quick Reference - Wrap conditional styles in `@media (condition) { }`. - Prefer **`min-width`** and write **mobile-first**. - Set breakpoints where **content** breaks, not where devices sit. - Use **`rem`** units so breakpoints respect user zoom. - Combine conditions with `and`, OR them with commas, and reach for **range syntax** (`width >= 48rem`) in modern projects. - Always include the **viewport meta tag**. - Use `prefers-color-scheme` and `prefers-reduced-motion` to respect user preferences for free. Master those seven habits and media queries stop being a source of mystery bugs and become the reliable backbone of every responsive site you build. ## Frequently asked questions **How do I use media queries in CSS?** Write your base styles normally, then wrap conditional styles in an @media block that names a feature and value, such as @media (min-width: 768px) { .card { padding: 2rem; } }. The browser applies those rules only when the viewport meets the condition. Include the viewport meta tag in your HTML head so mobile browsers report the real width and the queries fire correctly. **What is the difference between min-width and max-width media queries?** min-width applies styles at that width or wider, so styles cascade upward from small to large screens (mobile-first). max-width applies at that width or narrower, cascading downward from large to small (desktop-first). min-width is the modern default because base styles stay lean and the smallest, slowest devices load the least override CSS. Avoid pairing both on the same value to prevent boundary overlap. **What are the standard media query breakpoints?** Common min-width breakpoints are 640px (large phones), 768px (tablets), 1024px (laptops), 1280px (desktops), and 1536px (large monitors), roughly matching popular frameworks. But these are only a starting skeleton. Set breakpoints where your content actually breaks, not where specific devices sit, so your layout looks intentional on any screen size including hardware that does not exist yet. **Why are my media queries not working?** The most common cause is a missing viewport meta tag. Without in your HTML head, mobile browsers assume roughly 980px width and zoom out, so width-based queries test the wrong number. Other causes include specificity conflicts, source order (a later rule overriding your query), or overlapping min-width and max-width ranges that collide at the boundary. **Should I use px, em, or rem in media queries?** Prefer rem or em so breakpoints scale with the user's browser font-size preference, which is better for accessibility. A 768px breakpoint becomes 48rem (768 divided by 16). Note that em and rem in media queries are always relative to the browser default font size, not any element's font size, so both behave identically here. px breakpoints ignore user zoom, so avoid them for width thresholds. **What is the difference between media queries and container queries?** Media queries respond to the viewport, the whole browser window, making them right for page-level layout, global spacing, dark mode, and motion preferences. Container queries (@container) respond to the size of a parent element, so a component adapts to the space it actually occupies rather than the screen. Use container queries for reusable components that appear in sidebars, grids, or varying widths. --- # Native CSS Nesting vs Sass Nesting: What Changed and What to Use > Sass nesting is a build-time feature that compiles to flat CSS and glues selectors as text, so `&__title` becomes `.card__title`. Native CSS nesting is a real browser feature with no build step, where `&` is a live selector reference that behaves like `:is()` and cannot do BEM concatenation. For new projects on modern browsers, native CSS nesting is usually enough. Source: https://thesassway.com/css-nesting-vs-sass-nesting/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## The short answer Native CSS nesting and Sass nesting look almost identical, but they are not the same feature. Sass nesting is a **build-time convenience**: your `.scss` compiles to flat CSS, so it runs everywhere and the `&` symbol does literal string gluing. Native CSS nesting is a **real browser feature**: the `&` is a live selector reference (it behaves like `:is()`), there is no compile step, and it needs a reasonably modern browser to work. For most new projects in 2026, native CSS nesting is enough on its own. You reach for Sass nesting when you need BEM-style `&__element` concatenation, when you must support older browsers without a build step, or when nesting is just one reason among many that you already use Sass (variables, mixins, `@each` loops, partials). Here is the same component written both ways. ```scss // Sass (SCSS) .card { padding: 1rem; &__title { // compiles to .card__title font-weight: 700; } &:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } } ``` ```css /* Native CSS */ .card { padding: 1rem; & .title { /* compiles to nothing — this IS the output */ font-weight: 700; } &:hover { box-shadow: 0 2px 8px rgb(0 0 0 / 0.15); } } ``` Notice the difference already: Sass can build `.card__title` from `&__title`, but native CSS cannot. That single limitation drives most real-world decisions. ## The differences that actually matter | Behavior | Native CSS nesting | Sass nesting | | --- | --- | --- | | Runs in the browser | Yes, no build step | No — compiles to flat CSS first | | `&__element` BEM concatenation | Not supported | Supported | | What `&` means | Live reference, acts like `:is()` | Literal text substitution | | Specificity of `&` | Highest in the parent list | Each selector keeps its own | | Nest `@media` / container queries | Yes | Yes | | Browser support needed | Modern browsers only | None — output is plain CSS | | Variables, mixins, loops | No | Yes | ### 1. BEM concatenation only works in Sass This is the biggest practical gap. In Sass, `&` is glued to whatever follows it as plain text: ```scss .card { &__title { color: navy; } // -> .card__title &--featured { border: 2px solid gold; } // -> .card--featured } ``` Native CSS cannot do this. In the browser, `&` is a selector, not a string, so `&__title` does not produce `.card__title` — it is read as the parent followed by a type selector named `__title`, which matches nothing useful. If your codebase leans on BEM naming, native nesting will not replace that part of Sass. You either keep Sass for the concatenation or write the full class names out: ```css .card { padding: 1rem; } .card__title { color: navy; } /* just write it flat */ ``` ### 2. `&` and specificity behave differently In Sass, `&` is substituted before the CSS ever reaches the browser, so each generated selector carries its own specificity. In native CSS, `&` matches like the `:is()` pseudo-class, which takes the **highest specificity in the parent selector list**. That changes the math when a rule has several parent selectors: ```css #app, .menu { & a { color: teal; } } /* & a behaves like :is(#app, .menu) a -> specificity is driven by #app (an ID) for BOTH */ ``` The equivalent Sass would emit `#app a, .menu a` as two separate selectors, so `.menu a` keeps its low class-level specificity. If you migrate Sass nesting to native CSS and mix IDs and classes in one parent list, some rules can suddenly win or lose the cascade. When specificity surprises you, our guide to [CSS specificity](/css-specificity-explained/) walks through exactly how the browser scores these. ### 3. Native nesting has no variables, loops, or mixins Nesting is one feature. Sass is a whole language. Native CSS nesting gives you the visual grouping and the `&`, but it does not give you `@mixin`, `@include`, `@function`, `@each`, maps, or math. CSS custom properties cover a lot of what people used Sass variables for, but they do not replace design-token loops or a `@mixin` that stamps out a media-query breakpoint. If nesting is the *only* Sass feature you use, native CSS can retire your preprocessor. If you use the rest, keep Sass — see [Sass vs SCSS](/sass-vs-scss/) for how the two syntaxes fit into that bigger toolkit. ### 4. Both can nest media and container queries One of the nicest wins of nesting — in either flavor — is keeping a component's responsive rules next to the component instead of scattered at the bottom of the file. Both syntaxes support it, and they read almost identically: ```css /* Native CSS */ .card { display: grid; gap: 1rem; @media (min-width: 768px) { grid-template-columns: 1fr 2fr; } @container (min-width: 400px) { gap: 2rem; } } ``` ```scss // Sass (SCSS) — same shape .card { display: grid; gap: 1rem; @media (min-width: 768px) { grid-template-columns: 1fr 2fr; } } ``` Native CSS also lets you nest container queries this way, which pairs well with modern component-based responsive design. If you are new to breakpoints, our guide to [CSS media queries](/css-media-queries/) covers the syntax in depth. ## Migrating Sass nesting to native CSS If you want to drop the build step, most Sass nesting ports over cleanly. The two things that break are BEM concatenation and mixed-specificity parent lists. A safe checklist: 1. **Find every `&__` and `&--`.** These are the concatenation cases native CSS cannot reproduce. Expand them into full class names (`.card__title`) or keep those files in Sass. 2. **Prefix nested selectors with `&` or a combinator.** Writing `& .title` rather than a bare `.title` keeps intent obvious and avoids the oldest parser edge cases. 3. **Check parent selector lists that mix IDs and classes.** Because native `&` acts like `:is()`, split `#app, .menu { & a {...} }` into separate rules if the shared specificity would change which style wins. 4. **Keep `@media` and `@include`-free nesting as-is.** Plain grouping, `&:hover`, `&:focus`, and nested media queries all translate one-to-one. Anything that used `@mixin`, `@function`, `@each`, or Sass math is not a nesting concern — that is the rest of the language, and native CSS has no equivalent. ## Browser support in 2026 Native CSS nesting is **Baseline widely available**. The `&` nesting syntax shipped across the board in 2023 (Chrome 112, Safari 16.5, Firefox 117), and the more forgiving "relaxed" syntax — which lets a nested rule start with a bare element selector like `p` without a leading `&` — landed shortly after (Chrome 120, Safari 17.2). By 2026 both are safe in every evergreen browser. The practical caveat is old browsers. If your analytics still show meaningful traffic from browsers older than late 2023, native nesting will silently fail for those users. Two safe paths: - **Keep the `&` prefix.** Writing `& .title` instead of a bare `.title` is a touch more explicit and sidesteps the earliest parsing quirks. It costs nothing and reads clearly. - **Downcompile with PostCSS.** The `postcss-nesting` plugin flattens native nesting into plain CSS at build time, so you write modern syntax and ship output that works everywhere — the same trick Sass has always used. Our [preprocessors vs PostCSS](/css-preprocessors-vs-postcss/) comparison covers where PostCSS fits. ## Rules that apply to both Whichever you choose, the same nesting mistakes bite you. - **Do not nest more than three levels deep.** Deep nesting produces long, brittle, over-specific selectors that are hard to override. The old Sass "inception rule" (never go more than four levels in) is still good advice for native CSS. - **Nesting mirrors your HTML structure, which is fragile.** A selector like `.nav .list .item .link` breaks the moment someone reorders the markup. Prefer a single meaningful class per component. - **Grouping is not free specificity.** Nesting `.title` inside `.card` produces `.card .title` — a descendant combinator, not a scoped variable. It still leaks to any `.title` anywhere inside the card. ## Which should you use? A quick decision guide: - **New project, modern browsers, no BEM:** use native CSS nesting. One less dependency, no build step, and the browser does the work. - **You use BEM `&__element` naming:** stay with Sass, or accept writing class names out in full. - **You already use Sass for variables, mixins, and partials:** keep nesting in Sass so your whole stylesheet speaks one language. The [complete guide to Sass](/complete-guide-to-sass/) shows how nesting sits alongside those features. - **You need modern syntax but old-browser output:** write native nesting and downcompile with `postcss-nesting`. The honest summary: native CSS nesting has closed most of the gap, and for a large share of projects it makes preprocessor nesting optional. But "nesting" was never the main reason to adopt Sass. If you only ever nested, CSS has caught up. If you use the rest of the language, nesting natively or in Sass is a style choice, not a migration. ## Frequently asked questions **Does CSS support nesting natively now?** Yes. Native CSS nesting is Baseline widely available in 2026. The `&` nesting syntax shipped in Chrome 112, Safari 16.5, and Firefox 117 back in 2023, and the relaxed syntax that lets a nested rule start with a bare element selector followed shortly after. No preprocessor or build step is required in any current evergreen browser. **Is Sass still needed if CSS has nesting?** Only if you use more than nesting. Native CSS nesting replaces Sass nesting for most projects, but Sass also provides variables, mixins, functions, `@each` loops, maps, and partials that CSS has no built-in equivalent for. If nesting was the single reason you used Sass, you can drop it. If you rely on the rest of the language, keep Sass. **What is the difference between & in CSS and Sass?** In Sass, `&` is literal text substitution done at compile time, so `&__title` concatenates into `.card__title`. In native CSS, `&` is a live selector reference that behaves like the `:is()` pseudo-class. It cannot concatenate strings, so BEM-style `&__element` naming does not work, and its specificity equals the highest selector in the parent list. **Can native CSS nesting do BEM concatenation?** No. BEM naming like `&__title` or `&--featured` only works in Sass, where `&` is glued to the following text before compilation. In native CSS, `&` is a selector, so `&__title` does not build `.card__title` and matches nothing useful. To use BEM with native nesting, write the full class names out flat or keep those files in Sass. **Can you nest media queries in native CSS?** Yes. Both native CSS nesting and Sass let you nest `@media` and `@container` queries directly inside a rule, keeping a component's responsive styles next to the component instead of at the bottom of the file. The syntax is nearly identical in both, and native CSS handles container queries this way too, which suits component-based responsive design. **How do I convert Sass nesting to native CSS?** Most nesting ports directly. The two things that break are BEM concatenation (`&__`, `&--`), which you must expand into full class names, and parent selector lists mixing IDs and classes, whose specificity changes because native `&` acts like `:is()`. Plain grouping, `&:hover`, and nested media queries translate one-to-one. Use `postcss-nesting` if you need old-browser output. --- # CSS Preprocessors vs PostCSS: Sass, Less, and Tailwind Compared > A CSS preprocessor (Sass, Less, Stylus) is a complete language that compiles to CSS with built-in variables, mixins, and loops. PostCSS is not a preprocessor: it's a plugin-driven tool that transforms plain CSS with only the features you add, like Autoprefixer. Tailwind is a utility-class framework built on PostCSS. They aren't mutually exclusive, and many teams combine Sass with PostCSS. Source: https://thesassway.com/css-preprocessors-vs-postcss/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Preprocessors vs PostCSS: The Short Answer A **CSS preprocessor** (Sass, Less, Stylus) is a self-contained language with its own syntax that compiles down to CSS. **PostCSS** is not a preprocessor at all: it's a tool that parses your CSS into a tree and hands it to plugins that transform it, so you assemble exactly the features you want. **Tailwind** is a third thing again: a utility-class framework that happens to be built on PostCSS. They are not mutually exclusive. The honest 2026 answer is that most teams either pick one preprocessor (usually Sass) *or* skip it and lean on modern CSS plus a couple of PostCSS plugins (usually Autoprefixer and a nesting shim). A growing number ship Tailwind and write very little hand-authored CSS at all. Here's how they actually compare and how to choose. | Tool | What it is | Syntax | Runtime cost | Best when | |------|-----------|--------|--------------|-----------| | **Sass/SCSS** | Full preprocessor language | `.scss` / `.sass` | Build-time only | You want variables, mixins, loops, partials in one package | | **Less** | Full preprocessor language | `.less` | Build-time only | Legacy Bootstrap 2/3 projects; JS-based theming | | **Stylus** | Full preprocessor language | `.styl` | Build-time only | You like terse, optional-punctuation syntax | | **PostCSS** | Plugin-driven transformer | Plain `.css` | Build-time only | You want à la carte features on standard CSS | | **Tailwind** | Utility framework (uses PostCSS) | HTML classes | Build-time only | You style in markup and want a small final bundle | ## What a CSS Preprocessor Actually Does A preprocessor is a superset language. You write in *its* syntax, run a compiler, and get plain CSS out. Nothing ships to the browser except the compiled result, so preprocessors have **zero runtime cost**. The features that made them essential for a decade: - **Variables** for colors, spacing, and fonts - **Nesting** to mirror your HTML structure - **Mixins** for reusable blocks of declarations - **Functions and math** for computed values - **Partials and `@use`/`@import`** to split styles across files - **Loops and conditionals** to generate repetitive rules ### Sass/SCSS Sass is the most widely used preprocessor by a wide margin. Almost everyone writes the **SCSS** syntax, which is a strict superset of CSS, so any valid CSS file is already valid SCSS. ```scss $brand: #2563eb; $space: 8px; .card { padding: $space * 2; border: 1px solid rgba($brand, 0.2); &__title { color: $brand; font-weight: 700; } } ``` If you're new to it, start with [what Sass is and how SCSS works](/what-is-sass/), then the [complete guide to Sass](/complete-guide-to-sass/). The modern engine is **Dart Sass**; the old LibSass and Ruby Sass are both discontinued, so make sure any tutorial you follow uses `@use` rather than the deprecated `@import`. ### Less Less predates Sass's dominance and powered older Bootstrap versions (Bootstrap moved to Sass in v4). It's still maintained and can even run in the browser, though you should never do that in production. Its syntax uses `@` for variables: ```less @brand: #2563eb; @space: 8px; .card { padding: (@space * 2); border: 1px solid fade(@brand, 20%); &__title { color: @brand; } } ``` Less is a fine tool but sees little new adoption. Pick it mainly to match an existing codebase. ### Stylus Stylus is the minimalist of the three. It makes braces, colons, and semicolons optional, which some developers love and others find hard to read on a team. It's capable but has a small and shrinking community, so support and hiring are harder. ## What PostCSS Actually Is PostCSS is the one that confuses people, so let's be precise: **PostCSS is not a language and not a preprocessor.** It's a Node tool that parses CSS into an abstract syntax tree, lets JavaScript plugins walk and rewrite that tree, then prints CSS back out. On its own, PostCSS does nothing at all. The value is entirely in the plugins you choose: - **Autoprefixer** adds vendor prefixes based on your Browserslist config - **postcss-preset-env** lets you use future/modern CSS and polyfills it for older browsers - **postcss-nesting** compiles nesting for browsers that lack it - **cssnano** minifies the output - **postcss-import** inlines `@import` at build time A minimal setup: ```js // postcss.config.js module.exports = { plugins: { 'postcss-import': {}, 'postcss-nesting': {}, autoprefixer: {}, cssnano: {}, }, }; ``` Because you compose it yourself, PostCSS can act *like* a preprocessor (with nesting and custom-property plugins), *like* a postprocessor (prefixing, minifying), or both. That flexibility is the whole point, and also the catch: there's no single "PostCSS way," so two projects using it can look nothing alike. ### The key mental model A preprocessor is **opinionated and complete** out of the box. PostCSS is **unopinionated and empty** until you add plugins. One is a full meal; the other is a plate you fill yourself. ## Where Tailwind Fits Tailwind CSS is often lumped into this comparison, but it answers a different question. It doesn't give you a better way to *write* CSS; it largely replaces writing CSS with composing **utility classes** in your markup: ```html

Card title

``` Under the hood, recent Tailwind scans your templates and generates only the utilities you actually use, so the shipped stylesheet stays small. It's built on PostCSS but you rarely touch PostCSS directly. Tailwind and a preprocessor aren't really competitors: some teams run Tailwind for layout and spacing while still using a little Sass for the rare complex component. The real trade-off is philosophical — HTML-centric utilities versus hand-authored semantic classes — not a tooling detail. ## Do You Still Need a Preprocessor in 2026? This is the honest crux of the whole debate. Native CSS has absorbed most of what made preprocessors indispensable: | Feature | Preprocessor gave you | Native CSS status (2026) | |---------|----------------------|--------------------------| | Variables | `$brand: ...` | `--brand: ...` custom properties, fully supported | | Nesting | `.a { .b {} }` | Native CSS nesting, broad modern-browser support | | Color functions | `lighten()`, `rgba()` | `color-mix()`, relative color syntax | | Math | `$x * 2` | `calc()`, plus `min()` `max()` `clamp()` | | Scoping | conventions | `@scope`, `@layer` cascade layers | Custom properties even do something Sass variables can't: they're **live at runtime**, so they update with media queries, `:hover`, and JavaScript. Sass variables are frozen at compile time. So what's left that native CSS still doesn't do? Genuine programmatic power: **loops** (`@each`, `@for`), **mixins with logic**, **build-time partials with tree-shaking via `@use`**, and generating large sets of rules from a data map. If your design system leans on those, Sass still earns its place. If you mostly wanted variables and nesting, you may no longer need any preprocessor. For a deeper look at that specific overlap, see [native CSS nesting vs Sass nesting](/css-nesting-vs-sass-nesting/). ## How to Choose: A Practical Decision Guide **Choose Sass/SCSS if:** - You want one batteries-included tool with variables, mixins, loops, and partials - You're building or maintaining a design system with generated utility sets - Your team already knows it (it's the most common preprocessor by far) **Choose PostCSS (no preprocessor) if:** - You're comfortable with modern native CSS and want only a few targeted transforms - You mainly need Autoprefixer and a nesting/preset-env safety net - You value a lean toolchain and minimal new syntax to learn **Choose Tailwind if:** - Your team is happy styling in markup with utilities - You want a very small final CSS bundle with enforced design constraints - Rapid prototyping and consistency matter more than semantic class names **Choose Less or Stylus mainly if:** - You're maintaining a codebase that already uses them ### You'll often use two together This isn't either/or. A very common, entirely valid stack is **Sass for authoring plus PostCSS for finishing**, Sass gives you the language, then Autoprefixer and cssnano run over the compiled output. Most modern build tools (Vite, for instance) support both with almost no config, and Vite even compiles `.scss` natively once you install Sass. See [how to install and compile Sass](/how-to-install-sass/) to wire it up. ## Cost and Overhead All of these tools are **free and open source**, so there's no license cost. The real cost is complexity and build time: - **Preprocessors** add one dependency and a compile step. Dart Sass is fast; compiling even large projects is typically well under a second on incremental builds. - **PostCSS** cost scales with plugin count. A lean setup is negligible; a dozen plugins adds measurable build time. - **Tailwind** adds a scanning step but usually *reduces* shipped CSS, often to a few kilobytes gzipped for a typical site. Numbers vary widely by project size, machine, and configuration, so benchmark your own build rather than trusting a blanket figure. For most teams the build-time overhead of any of these is not the deciding factor; **maintainability and team familiarity are.** The larger hidden cost is cognitive. Every tool you add is one more syntax, config file, and failure mode a new teammate has to learn before they can ship a style change. A preprocessor asks people to learn a language; a big PostCSS setup asks them to understand which of a dozen plugins produced a given output; Tailwind asks them to memorize a utility vocabulary. None of these is wrong, but the cheapest stack to onboard onto is usually the smallest one that still does what you need. ### Migrating between them Moving off a preprocessor is easier than it used to be, because SCSS is a superset of CSS. You can rename `.scss` files to `.css`, then swap `$variables` for `--custom-properties` and nested rules for native nesting incrementally, one component at a time, with no big-bang rewrite. Going the other direction, adopting Sass in a plain-CSS project, is equally low-risk since your existing CSS is already valid SCSS on day one. ## Bottom Line Preprocessors, PostCSS, and Tailwind solve overlapping but distinct problems. Sass is the safe, powerful default when you need real programming features in your styles. PostCSS is the flexible, à la carte choice when modern CSS gets you 90% of the way and you just need a few transforms. Tailwind is a different authoring philosophy entirely. Native CSS has closed much of the old gap, so the most defensible modern setups are often "Sass where you need logic" or "plain CSS plus a thin PostCSS layer", and plenty of teams run both. Pick based on the features you actually use and what your team can maintain, not on which tool trended last year. ## Frequently asked questions **Is PostCSS a preprocessor?** No. PostCSS is a Node tool that parses CSS into a syntax tree and lets plugins transform it. On its own it does nothing; its behavior comes entirely from plugins like Autoprefixer, postcss-nesting, or cssnano. It can act like a preprocessor, a postprocessor, or both, depending on which plugins you add, which is why it's more accurate to call it a transformer than a preprocessor. **What is the difference between Sass and PostCSS?** Sass is a complete, opinionated language you write in, then compile to CSS, with variables, mixins, and loops built in. PostCSS is an unopinionated tool that transforms plain CSS using plugins you choose yourself. Sass is a full meal out of the box; PostCSS is an empty plate you fill. Many teams use both: Sass to author styles and PostCSS to prefix and minify the output. **Do I still need a CSS preprocessor in 2026?** Often not. Native CSS now has custom properties, nesting, color-mix(), calc(), cascade layers, and @scope, covering most of what preprocessors were used for. You still benefit from a preprocessor like Sass if you rely on loops, mixins with logic, or generating large rule sets from data. If you mainly wanted variables and nesting, modern CSS plus a small PostCSS setup may be enough. **Sass vs Less vs PostCSS: which is best?** Sass is the most widely used and the safest default for a full-featured preprocessor. Less is capable but sees little new adoption, so choose it mainly to match an existing codebase. PostCSS isn't a direct competitor; it's a flexible transformer for teams comfortable with modern CSS who want only a few targeted features. There's no single best, only the best fit for your features and team. **Is Tailwind a CSS preprocessor?** No. Tailwind is a utility-class framework, not a preprocessor. Instead of writing CSS in a superset language, you compose pre-made utility classes directly in your HTML. It's built on top of PostCSS and generates only the utilities you actually use, keeping the final bundle small. Some teams even pair Tailwind with a little Sass for complex components, since they solve different problems. **Can you use Sass and PostCSS together?** Yes, and it's a common setup. Sass handles authoring with variables, mixins, and partials, then PostCSS runs over the compiled CSS to add vendor prefixes with Autoprefixer and minify with cssnano. Modern build tools like Vite support both with almost no configuration. They operate at different stages, so combining them is straightforward and widely recommended for production pipelines. --- # CSS Specificity Explained: How the Cascade Decides Which Style Wins > CSS specificity is the scoring system browsers use to decide which rule wins when several target the same element. Each selector scores three columns: IDs, then classes/attributes/pseudo-classes, then element types. Compared left to right, a higher column always wins, so one ID beats any number of classes. When scores tie, the rule written later in the source applies. Source: https://thesassway.com/css-specificity-explained/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## CSS Specificity, in One Sentence When two CSS rules set the same property on the same element, **specificity is the scoring system the browser uses to decide which rule wins**. Every selector earns a score from three columns — IDs, classes, and element types. The rule with the higher score applies. When scores tie, the rule written **later** in the source wins. That single mechanism explains almost every "why won't this style apply?" moment you have ever had. Specificity is one of the three questions the cascade asks, alongside origin/importance and source order. If you are still shaky on how CSS decides anything at all, start with [what CSS actually is](/what-is-css/) and come back — this guide assumes you know how a rule is structured. ## How the Score Is Calculated Think of every selector's specificity as three numbers, written left to right, most powerful first: `(IDs, classes, elements)` - **Column 1 — IDs:** each `#id` in the selector adds 1. - **Column 2 — classes, attributes, and pseudo-classes:** each `.class`, `[attr]`, or `:hover`/`:focus`/`:nth-child()` adds 1. - **Column 3 — element types and pseudo-elements:** each tag name (`div`, `a`, `li`) and `::before`/`::after` adds 1. You compare left to right. A single ID beats **any** number of classes, and a single class beats **any** number of element selectors. The columns do not carry over — 11 classes is `(0, 11, 0)`, which still loses to one ID at `(1, 0, 0)`. It is not base-10 math; it is column-by-column comparison. ### What counts for nothing Three things add **zero** specificity but often confuse people: - The **universal selector** `*` - **Combinators** — `>`, `+`, `~`, and the descendant space - The `:where()` pseudo-class (more on this powerful escape hatch below) So `* {}`, `ul > li {}` — the `>` adds nothing, but the two element names still score `(0, 0, 2)`. ## The Specificity Table You Can Bookmark | Selector | IDs | Classes | Elements | Score | |---|---|---|---|---| | `*` | 0 | 0 | 0 | (0,0,0) | | `li` | 0 | 0 | 1 | (0,0,1) | | `ul li` | 0 | 0 | 2 | (0,0,2) | | `.nav` | 0 | 1 | 0 | (0,1,0) | | `a:hover` | 0 | 1 | 1 | (0,1,1) | | `input[type="text"]` | 0 | 1 | 1 | (0,1,1) | | `.nav .link.active` | 0 | 3 | 0 | (0,3,0) | | `#header` | 1 | 0 | 0 | (1,0,0) | | `#header .nav a` | 1 | 1 | 1 | (1,1,1) | | `style="..."` (inline) | — | — | — | wins over all selectors | | any rule with `!important` | — | — | — | wins over all non-important rules | Read that table top to bottom and you have the whole hierarchy: elements < classes/attributes/pseudo-classes < IDs < inline styles < `!important`. ## Worked Examples: Which Rule Wins? Say you have this markup: ```html
Sign up ``` And these four rules, all setting `color`: ```css a { color: blue; } /* (0,0,1) */ .button { color: green; } /* (0,1,0) */ .button.primary { color: teal; } /* (0,2,0) */ #cta { color: red; } /* (1,0,0) */ ``` The link renders **red**. `#cta` scores `(1, 0, 0)`, and that single ID outranks every class-based rule no matter how many classes you stack. Delete the `#cta` rule and the winner becomes **teal** — `.button.primary` at `(0, 2, 0)` beats `.button` at `(0, 1, 0)`. ### The tie-breaker: source order When two selectors have the **identical** score, the last one in the stylesheet wins. This is why order matters: ```css .button { color: green; } .button { color: orange; } /* this one wins — same score, written later */ ``` Both are `(0, 1, 0)`. Nothing separates them but position, so the later declaration takes effect. This is also why linking your own stylesheet *after* a framework like Bootstrap lets your same-specificity overrides land. ## `!important` and Inline Styles: Outside the Normal Game Two things sit above the ordinary three-column score. **Inline styles** — a `style="color: purple"` attribute on the element — behave as if they have a specificity higher than any selector. In the older four-column model this was written `(1, 0, 0, 0)`. Practically: an inline style beats any rule in your stylesheet that targets the same property, unless that rule uses `!important`. **`!important`** is the true trump card. A declaration flagged important is lifted into a separate, higher-priority tier of the cascade: ```css .button { color: red !important; } #cta { color: blue; } /* loses — the important red wins */ ``` Even though `#cta` has a higher raw score, `!important` moves `.button` into a layer that outranks all normal declarations. When **two** important rules collide, the browser falls back to comparing their specificity normally. > **Use `!important` as a last resort, not a habit.** Every important you add is a rule that can only be beaten by another important, and specificity wars spiral fast. Reach for it to override third-party CSS you cannot edit, or for genuine utility overrides — not to patch your own cascade. ## Why Your CSS Isn't Applying (The Real Reasons) "Why is my CSS not applying?" is almost always one of these five, roughly in order of how often they bite: 1. **A more specific rule is overriding you.** Open DevTools, inspect the element, and look at the Styles panel — the losing rule appears with a **strikethrough**. That is the browser telling you exactly what won. 2. **Two rules tie and yours comes first.** Move your rule later or increase its specificity by one class. 3. **An `!important` upstream** is beating your normal rule. Match it deliberately or, better, remove the upstream important. 4. **The selector doesn't match** what you think — a typo, wrong class name, or an element that isn't actually a descendant. 5. **The property is inherited or invalid,** so nothing is "winning" at all — the value you wrote was never parsed. DevTools is the fastest specificity debugger you own. The struck-through declarations and the top-to-bottom order in the Styles panel *are* the cascade, rendered visually. ## Modern Selectors That Change the Math Three newer pseudo-classes give you direct control over specificity. All three are supported in every current browser (Chrome, Edge, Firefox, and Safari). ### `:where()` — zero specificity on purpose `:where()` always contributes **(0, 0, 0)**, no matter what you put inside it. This is the modern tool for writing defaults that are trivially easy to override: ```css :where(.card) a { color: rebeccapurple; /* specificity (0,0,1) — just the `a` counts */ } ``` Because the `.card` scope adds nothing, any later `.card a { color: ... }` rule at `(0, 1, 1)` wins effortlessly. Design-system and reset authors lean on `:where()` heavily so their base styles never fight application code. ### `:is()` and `:not()` — take the highest argument `:is()` and `:not()` adopt the specificity of their **most specific** argument: ```css :is(#main, .content) p { } /* the #main pushes this to (1,0,1) */ ``` Even if the element matched via `.content`, the whole selector scores as if the ID applied. `:not(.hidden)` scores like `.hidden` — one class. Knowing this stops `:is()` from silently inflating your specificity. | Pseudo-class | Specificity contribution | |---|---| | `:where(anything)` | always (0,0,0) | | `:is(#id, .class)` | the most specific argument — here (1,0,0) | | `:not(.class)` | the most specific argument — here (0,1,0) | ## Cascade Layers: Specificity's New Boss `@layer` (supported in all current browsers) changes the order of operations. Rules in a **later-declared layer beat rules in an earlier layer regardless of specificity**: ```css @layer reset, base, components; @layer base { #sidebar a { color: black; } /* (1,0,0) — but lower layer */ } @layer components { .link { color: blue; } /* (0,1,0) — wins anyway */ } ``` Here `.link` wins even though `#sidebar a` has a far higher raw score, because `components` is declared after `base`. Layers let you retire ID-versus-class specificity battles entirely by deciding priority at the *layer* level instead. Unlayered styles beat all layered styles, which keeps the model predictable. ## Keeping Specificity Low and Sane The teams with the fewest cascade headaches follow a handful of habits: - **Prefer classes.** A flat world of single-class selectors — the spirit behind BEM naming — means almost everything scores `(0, 1, 0)` and source order settles ties predictably. - **Avoid IDs for styling.** Reserve `id` for JavaScript hooks and anchors; its `(1, 0, 0)` weight is hard to override on purpose. - **Don't over-nest.** `.header .nav ul li a` is fragile and heavy. If you write Sass, watch your nesting depth — it is the single easiest way to accidentally manufacture high-specificity selectors. Custom properties help here too; see our guide to [CSS variables done right](/css-variables-guide/) for theming without specificity creep. - **Use `:where()` for resets and defaults** so base styles never block later overrides. - **Adopt `@layer`** on larger projects to make priority explicit instead of emergent. Specificity is not really about memorizing a formula — it is about writing selectors flat enough that you rarely have to calculate anything. Once your rules mostly live in the same column, the cascade becomes boringly predictable, which is exactly what you want. From here, the natural next stop is how those winning rules actually size and space your elements — the [CSS box model](/css-box-model/) picks up right where specificity leaves off. ## Frequently asked questions **How is CSS specificity calculated?** Count three things in your selector: IDs, then classes/attributes/pseudo-classes, then element types and pseudo-elements. Write them as (a,b,c) and compare left to right. A higher first column always wins, so a single ID (1,0,0) beats eleven classes (0,11,0). Columns never carry over into base-10 math; each is compared independently. **What is the order of specificity in CSS?** From weakest to strongest: the universal selector and combinators (zero), element types and pseudo-elements, then classes, attributes, and pseudo-classes, then IDs, then inline style attributes, and finally !important on top. Cascade layers and source order break ties, with later layers and later-written rules winning when raw scores are equal. **Does an ID always beat a class in CSS?** Yes, in normal specificity comparison. A single ID selector scores (1,0,0) and outranks any number of class selectors, because the browser compares the ID column before the class column. The only ways a class rule beats an ID rule are !important, an inline style, or a later cascade layer that reorders priority entirely. **Why is my CSS not applying?** Usually a more specific rule is overriding yours, or two rules tie and yours comes first in the source. Open DevTools and inspect the element: the losing declaration shows with a strikethrough. Other causes are an upstream !important, a selector that doesn't actually match, or an invalid property value that was never parsed. **How do I override a rule that uses !important?** The cleanest fix is to remove the upstream !important so normal specificity resumes. If you cannot edit that CSS, add your own declaration with !important and a specificity equal to or higher than the original, since important rules are compared against each other by ordinary specificity. Cascade layers can also help you avoid the fight altogether. **Does !important override inline styles?** Yes. An !important declaration in a stylesheet beats a normal inline style attribute. However, an inline style that itself uses !important outranks an important rule in a stylesheet. In practice the safest hierarchy to remember is: normal styles, then inline styles, then important stylesheet rules, then important inline styles at the very top. --- # CSS Units Explained: px vs em vs rem vs % vs vw > CSS units are either absolute (px, which never changes) or relative (em, rem, %, vw, vh, which scale against a reference). Use rem for font sizes and spacing because it respects user settings, px for borders and fine details that shouldn't scale, % for fluid widths, and vw/vh for viewport-based sizing like full-height sections and fluid type. Source: https://thesassway.com/css-units-guide/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## The Short Answer: Which CSS Unit to Use CSS units fall into two camps: **absolute** units like `px` that never change, and **relative** units like `em`, `rem`, `%`, `vw`, and `vh` that scale against something else. The right choice depends on what you want to happen when a user zooms, changes their default font size, or resizes the window. Here is the rule most professionals actually follow: - **`rem`** for font sizes, spacing, and most layout values — it respects the user's browser settings and stays predictable. - **`px`** for things that should never scale: `1px` borders, hairline dividers, box shadows. - **`%`** for widths inside a flexible container. - **`em`** for spacing that should track the element's own font size (padding on a button, for example). - **`vw` / `vh`** for things sized against the viewport: full-height hero sections, fluid typography. Everything below explains *why* each unit behaves the way it does, so you can reach for the right one without guessing. ## Absolute vs Relative Units An **absolute unit** is fixed. A `px` is a `px` no matter where you put it or what the user does. Absolute units include `px`, `cm`, `mm`, `in`, `pt`, and `pc`, but in practice `px` is the only one you'll use on screen — the physical units (`cm`, `in`, `pt`) are meant for print stylesheets. A **relative unit** measures against a reference point. Change the reference and the computed value changes with it. This is what makes relative units powerful for responsive and accessible design: one rule can adapt to many contexts. | Unit | Type | Relative to | Best for | |------|------|-------------|----------| | `px` | Absolute | Nothing (fixed) | Borders, shadows, fine details | | `em` | Relative | The element's own `font-size` | Padding/margins that track text | | `rem` | Relative | The root (``) `font-size` | Font sizes, spacing, layout | | `%` | Relative | The parent's corresponding value | Fluid widths | | `vw` | Relative | 1% of viewport width | Fluid type, full-width elements | | `vh` | Relative | 1% of viewport height | Full-height sections | ## px: The Fixed Unit `px` (pixel) is the CSS reference pixel. On modern high-density screens it isn't literally one hardware dot, but it stays visually consistent across devices. ```css .card { border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } ``` **The upside:** total predictability. `16px` is `16px` everywhere. **The downside:** it ignores user preferences. If someone bumps their browser's default font size up to 20px because they have trouble reading small text, a `font-size: 16px` rule stays stubbornly at 16. That's an accessibility problem. (Modern browsers still enlarge px on *page zoom*, but not when a user changes their default text size — the two are different settings.) Use `px` for details that genuinely shouldn't scale with text, and reach for a relative unit for anything typographic. ## em: Relative to the Element `1em` equals the current element's `font-size`. If an element's font size is `20px`, then `1em` is `20px` *for that element* — for padding, margins, widths, and even for a nested font size. ```css .button { font-size: 18px; padding: 0.75em 1.5em; /* 13.5px vertical, 27px horizontal */ } ``` Because the padding is in `em`, a bigger button (larger font size) automatically gets proportionally bigger padding. That's exactly what you want for a component that ships at several sizes. ### The em compounding trap `em` inherits, and that causes surprises when you nest. Font sizes multiply down the tree: ```css .menu { font-size: 1.2em; } .menu li { font-size: 1.2em; } /* now 1.2 × 1.2 = 1.44em of the parent */ ``` Three levels deep and your text is unexpectedly huge. This compounding is the single biggest reason developers moved font sizing to `rem`. ## rem: Relative to the Root `rem` means "root em." It's always relative to the `font-size` of the `` element — no compounding, no matter how deep you nest. Since the browser default root size is `16px`, `1rem` is `16px` unless you change it. ```css :root { font-size: 100%; /* = 16px by default; respects user settings */ } h1 { font-size: 2rem; } /* 32px */ p { font-size: 1rem; } /* 16px */ small{ font-size: 0.875rem; }/* 14px */ ``` `rem` is the modern default for typography and spacing because it gives you two things at once: **predictability** (one reference point) and **accessibility** (it scales when a user changes their default font size). A handy trick is to build your spacing scale in `rem` so the whole layout breathes together when the root size changes. If you're managing a design system, pair `rem` sizing with [CSS custom properties](/css-variables-guide/) so your scale lives in one place: ```css :root { --space-1: 0.5rem; --space-2: 1rem; --space-3: 1.5rem; } ``` ## Percentages: Relative to the Parent `%` is relative to the parent's corresponding property — but *which* property matters, and it trips people up: - `width: 50%` → 50% of the parent's **width**. - `height: 50%` → 50% of the parent's **height** (only works if the parent has an explicit height). - `padding: 10%` and `margin: 10%` → always relative to the parent's **width**, even for top and bottom values. That last rule is the classic gotcha. Vertical padding in percent is calculated from width, which is actually useful for maintaining aspect ratios but confusing the first time you meet it. ```css .column { width: 33.333%; /* three across in a flexible row */ } ``` Percentages shine for fluid widths inside a container. For layout structure, though, Flexbox and Grid usually give you cleaner control — see how sizing interacts with spacing in the [CSS box model](/css-box-model/). ## vw and vh: Relative to the Viewport Viewport units measure against the browser window itself: - `1vw` = 1% of the viewport **width** - `1vh` = 1% of the viewport **height** - `1vmin` = 1% of the **smaller** dimension - `1vmax` = 1% of the **larger** dimension ```css .hero { min-height: 100vh; /* fills the screen height */ } ``` Viewport units are unbeatable for full-bleed sections and fluid effects. But a few caveats: - **`100vw` can cause horizontal scroll** because it includes the space a vertical scrollbar occupies. Prefer `100%` for full-width block elements. - **`100vh` on mobile** historically overshot because of the dynamic address bar. Modern CSS adds `svh`, `lvh`, and `dvh` (small, large, and dynamic viewport height) to handle this — `100dvh` adapts as the mobile toolbar shows and hides. Browser support for these is solid across current Chrome, Safari, Firefox, and Edge, but check your minimum-support target before shipping. ### Fluid typography with clamp() Viewport units really pay off when combined with `clamp()` for text that scales smoothly between a floor and a ceiling: ```css h1 { font-size: clamp(1.75rem, 1rem + 3vw, 3rem); } ``` This reads as "never smaller than `1.75rem`, never larger than `3rem`, and in between grow with the viewport." No media query needed. This technique pairs naturally with a [mobile-first CSS](/mobile-first-css/) approach, where you design for the small screen first and let type scale up. ## em vs rem: The Question Everyone Asks They differ in exactly one way — their reference point. | | `em` | `rem` | |--|------|-------| | Relative to | Element's own font-size | Root font-size | | Compounds when nested | Yes | No | | Best for | Component-local spacing | Global type & layout | | Predictability | Lower (context-dependent) | Higher (single source) | **Practical guidance:** use `rem` for font sizes across the site so nothing compounds unexpectedly. Use `em` inside a component when you *want* a value to track that component's text size — button padding, an icon sized to match the label, letter-spacing on a heading. ## px vs rem: Which for Font Size? Use `rem`. The reason is accessibility: a font size set in `px` ignores a user who has raised their browser's default text size, while `rem` honors it. Roughly 1 in 20 users runs a non-default font size, often because they need larger text — coding your type in `px` quietly locks them out. Keep `px` for the things that should stay crisp regardless of text size: borders, outlines, and shadow offsets. ## A Quick Decision Cheat-Sheet | You're sizing… | Reach for | |----------------|-----------| | Body and heading font sizes | `rem` | | Margins and padding (global spacing) | `rem` | | Padding that should track a component's text | `em` | | A `1px` border or hairline | `px` | | Column width in a flexible row | `%` | | Full-height hero section | `100dvh` (or `100vh` fallback) | | Type that scales with the screen | `clamp()` with a `vw` middle value | ## The Bottom Line You don't need every unit. A clean, accessible stylesheet mostly runs on **`rem` for type and spacing**, **`px` for fine details**, **`%` for fluid widths**, and **`vw`/`vh` (or the newer `dvh`) for viewport-scaled effects**. `em` earns its place for component-local sizing where you deliberately want values to follow the text. Get those defaults into your muscle memory and the "which unit?" question mostly answers itself. When you're ready to see how units interact with sizing and spacing in practice, the [CSS box model](/css-box-model/) is the next piece of the puzzle. ## Frequently asked questions **What is the difference between em and rem?** Both are relative units, but their reference differs. em is relative to the element's own font-size, so it compounds when you nest elements. rem is always relative to the root (html) font-size, so it never compounds. Use rem for global font sizes and spacing to stay predictable, and em when you want a value to track a specific component's text size. **Should I use px or rem for font size?** Use rem. A font size set in px ignores users who raise their browser's default text size, which is an accessibility problem. rem scales with that setting while staying predictable. Reserve px for details that shouldn't scale with text, such as 1px borders, outlines, and box-shadow offsets. **When should I use vw and vh?** Use viewport units when sizing against the browser window: 100vh (or 100dvh on mobile) for full-height hero sections, and a vw value inside clamp() for fluid typography that scales with screen width. Avoid 100vw for full-width blocks because it includes the scrollbar and can cause horizontal scroll; use 100% instead. **What is 1rem in pixels?** By default, 1rem equals 16px, because the browser's default root font-size is 16px. That default changes if the user adjusts their browser text-size setting or if you set a different font-size on the html element. This is exactly why rem is accessible: it honors the user's chosen base size rather than locking it. **What does percentage refer to in CSS?** A percentage is relative to the parent's corresponding property. width: 50% is half the parent's width, and height: 50% is half its height (if the parent has an explicit height). The catch: padding and margin percentages are always calculated from the parent's width, even for top and bottom values. **Are px still used in modern CSS?** Yes, but selectively. px is ideal for values that should stay fixed regardless of text scaling: 1px borders, hairline dividers, shadow offsets, and outlines. For anything typographic or layout-related that should adapt to users and screens, relative units like rem, %, and viewport units are the modern default. --- # How to Use CSS Custom Properties (Variables) the Right Way > CSS variables, or custom properties, store a reusable value declared with a double-hyphen name like --brand and read with the var() function. Unlike Sass variables, they live in the browser at runtime: they inherit through the DOM, respond to media queries, and can be read or changed with JavaScript. Every evergreen browser has supported them since 2017. Source: https://thesassway.com/css-variables-guide/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## What CSS Custom Properties Actually Are CSS custom properties, better known as **CSS variables**, let you store a value once and reuse it anywhere in your stylesheet. You declare one with a double-hyphen name and read it back with the `var()` function: ```css :root { --brand: #6c5ce7; --space: 1rem; } .button { background: var(--brand); padding: var(--space); } ``` The one detail that trips people up first: the `--` prefix is required and the name is case-sensitive. `--Brand` and `--brand` are two different variables. Unlike Sass variables, custom properties are **live in the browser**. They exist in the DOM at runtime, they inherit through the element tree, they respond to media queries, and JavaScript can read and change them on the fly. That single difference is what makes them worth learning even if your project already uses Sass. Every evergreen browser has supported custom properties since 2017, so you can use them in production today without a fallback for the general case. The features to check individually are `@property` and style container queries, which arrived later. ## How `var()` Works The `var()` function takes the name of a custom property and, optionally, a fallback value: ```css .card { color: var(--text-color, #333); } ``` If `--text-color` is defined anywhere up the tree, its value is used. If it is not defined, `#333` is used instead. The fallback is not an error handler for typos in your CSS values; it only fires when the variable itself is missing. ### Custom Properties Inherit This is the behavior that separates them from preprocessor variables. A custom property set on an element cascades down to its descendants, exactly like `color` or `font-family`: ```css .alert { --accent: #e17055; border-left: 4px solid var(--accent); } .alert .badge { background: var(--accent); /* inherits #e17055 */ } ``` You can override the value at any level, and everything below that point picks up the new value. That is how a single component can be re-themed just by resetting one property on its root element. ### Scoping to a Selector Declaring variables on `:root` makes them global, but you rarely want everything global. Scope a variable to the component that owns it: ```css .pricing-card { --card-bg: white; --card-pad: 1.5rem; background: var(--card-bg); padding: var(--card-pad); } .pricing-card.featured { --card-bg: #2d3436; color: white; } ``` The `.featured` modifier changes one variable, and the background follows. No duplicated `background` rule, no override specificity war. ## CSS Variables vs Sass Variables This is the comparison developers search for most, and the honest answer is that they solve different problems. Sass variables (`$name`) are resolved when your SCSS compiles to CSS and then vanish. CSS variables live on in the browser. | Feature | Sass variable (`$name`) | CSS variable (`--name`) | | --- | --- | --- | | Exists at runtime | No, compiled away | Yes, lives in the DOM | | Inherits through the DOM | No | Yes | | Readable/writable from JS | No | Yes | | Changes with media queries | No | Yes | | Usable in `calc()` | Yes | Yes | | Works without a build step | No | Yes | | Can be used in a Sass loop/map | Yes | Not directly | Use **Sass variables** for values the browser never needs to see: breakpoint numbers you feed into `@media`, a color map you iterate over to generate utility classes, or math done at build time. Use **CSS variables** for anything that changes at runtime: themes, component variants, values you tweak with JavaScript, or tokens that must respond to a media query. In practice, modern codebases mix both. A common pattern is to define a Sass map of design tokens and emit them as custom properties so you get build-time convenience and runtime flexibility. If you are weighing where each tool fits in a larger system, our guide to [Sass mixins vs functions](/sass-mixins-vs-functions/) covers the same build-time-versus-runtime line from the Sass side. ## Building a Theme Switcher The killer use case for custom properties is theming, because you change a handful of variables and the whole page re-paints. Define your tokens once, then override them under a data attribute or class: ```css :root { --bg: #ffffff; --fg: #1a1a1a; --muted: #666666; } :root[data-theme="dark"] { --bg: #12141a; --fg: #f4f4f5; --muted: #9ca3af; } body { background: var(--bg); color: var(--fg); } ``` Flipping the theme is a one-line JavaScript change: ```js document.documentElement.dataset.theme = "dark"; ``` Because every component reads `--bg` and `--fg` instead of hard-coded hex values, nothing else has to know a theme exists. To respect the user's operating-system preference automatically, pair it with a media query: ```css @media (prefers-color-scheme: dark) { :root { --bg: #12141a; --fg: #f4f4f5; } } ``` ## Custom Properties in `calc()` Variables compose cleanly with `calc()`, which unlocks fluid, token-driven spacing and sizing: ```css :root { --gap: 8px; } .stack > * + * { margin-top: calc(var(--gap) * 2); /* 16px */ } ``` A word of caution about units. A custom property holds a raw token, not a typed value, so this silently fails: ```css :root { --size: 24; } /* just the number 24 */ .box { width: var(--size)px; } /* INVALID — do not do this */ ``` The browser sees `24px` glued together only if you do it through `calc()`: ```css :root { --size: 24; } .box { width: calc(var(--size) * 1px); } /* 24px, correct */ ``` Storing the unit with the value (`--size: 24px;`) is usually cleaner and avoids the trap entirely. Reach for the `calc()` multiplication trick only when you genuinely need the bare number elsewhere, such as in an animation calculation. If you are unsure which unit belongs in your tokens, our [CSS units guide](/css-units-guide/) breaks down px, em, rem, and the viewport units. ## Reading and Writing Variables with JavaScript Because custom properties are real DOM values, JavaScript can read and set them without touching individual style rules: ```js const root = document.documentElement; // Read the computed value const brand = getComputedStyle(root).getPropertyValue("--brand"); // Write a new value root.style.setProperty("--brand", "#00b894"); ``` `getPropertyValue()` returns the value as a string, often with leading whitespace, so trim it before comparing. Setting a property with `setProperty()` writes an inline style on that element, which will override stylesheet declarations of the same variable thanks to normal specificity rules. This is the foundation for interactive color pickers, live spacing controls, and cursor-following effects. Custom properties also animate, which our [CSS animations tutorial](/css-animations-tutorial/) puts to work. ## The `@property` Rule Plain custom properties are untyped strings, which means the browser cannot smoothly animate between two values of `--brand` because it does not know they are colors. The `@property` at-rule fixes that by registering a type: ```css @property --angle { syntax: ""; inherits: false; initial-value: 0deg; } .spinner { transition: --angle 0.4s ease; transform: rotate(var(--angle)); } ``` Now the browser knows `--angle` is an angle and can interpolate it during transitions and animations. `@property` is supported across current Chrome, Edge, Safari, and Firefox, but confirm your browser targets before relying on it, since older versions ignore it and fall back to the initial value. ### A Note on Performance Custom properties are cheap, but they are not free. When you change a variable that many elements read, the browser has to recompute styles for every element in that variable's scope. Changing a global token on `:root` that hundreds of nodes depend on is fine for a one-off theme switch, but avoid doing it inside a high-frequency loop such as a scroll or mouse-move handler on a large page. If you need per-frame updates, scope the variable to the smallest element that actually uses it so the browser has less work to recalculate. ## Common Mistakes and How to Avoid Them A few patterns cause most of the confusion around custom properties: - **Expecting a fallback to catch bad values.** `var(--x, red)` only uses `red` when `--x` is undefined. If `--x` is set to `garbage`, the fallback does not run; the property is simply invalid and the declaration is ignored. - **Gluing units onto a variable.** `var(--n)px` is invalid. Use `calc(var(--n) * 1px)` or store the unit in the token. - **Overusing `:root`.** Global tokens are fine for a palette, but scope component-specific variables to the component so they cannot leak or collide. - **Forgetting the double hyphen.** `var(-brand)` and `background: brand` are silent no-ops. The `--` is mandatory. - **Assuming Sass will interpolate it.** Sass treats `var(--x)` as a plain string, so `lighten(var(--x), 10%)` will not work. Color functions need a real value, not a runtime reference. ## A Practical Naming Convention As a stylesheet grows, unstructured variable names become as hard to manage as the values they replaced. A light two-tier system keeps things readable: ```css :root { /* Primitive tokens: raw values */ --color-purple-500: #6c5ce7; --size-4: 1rem; /* Semantic tokens: intent, built from primitives */ --color-action: var(--color-purple-500); --space-inline: var(--size-4); } .button { background: var(--color-action); padding-inline: var(--space-inline); } ``` Components reference the **semantic** layer, never the raw hex. When the brand color changes, you edit one primitive and every button, link, and badge updates. This is the same discipline design systems use, and it scales from a personal project to a large team without a rewrite. ## When to Reach for Custom Properties Use CSS custom properties whenever a value needs to change at runtime, be shared across unrelated components, respond to a media query, or be driven by JavaScript. Keep Sass variables for pure build-time work like breakpoint math and generating classes in a loop. Most real projects use both, and knowing which job each one does is the difference between a stylesheet you fight and one that maintains itself. ## Frequently asked questions **What is the difference between CSS variables and Sass variables?** Sass variables ($name) are resolved when SCSS compiles to CSS and then disappear, so they are pure build-time values. CSS variables (--name) live on in the browser: they inherit through the DOM, react to media queries, and can be read or written with JavaScript. Use Sass variables for build-time math and loops, and CSS variables for anything that changes at runtime, like themes. **How do you use var() in CSS?** Call var() with a custom property name to read its value: color: var(--text). You can add a fallback as a second argument, like var(--text, #333), which is used only when --text is undefined. The fallback does not catch invalid values; if the variable is set to something the property cannot parse, the whole declaration is simply ignored. **Are CSS custom properties supported in all browsers?** Yes for the core feature. Chrome, Edge, Firefox, and Safari have all supported CSS custom properties and var() since 2017, so no fallback is needed for typical use. Newer additions layered on top, such as the @property at-rule for typed, animatable variables, arrived later, so check current browser support before relying on those specific features. **Can you change a CSS variable with JavaScript?** Yes. Read a value with getComputedStyle(element).getPropertyValue('--name'), which returns a string you should trim. Set a value with element.style.setProperty('--name', value), which writes an inline style that overrides stylesheet declarations of the same variable. This makes custom properties ideal for interactive color pickers, live spacing controls, and theme toggles. **Why is my CSS variable not working?** The most common causes are a missing double hyphen (the -- prefix is required and names are case-sensitive), gluing a unit directly onto var() like var(--n)px which is invalid, or referencing a variable that was never defined in the current scope. Also remember that a fallback only fires when the variable is undefined, not when its value is unparseable. **How do you build a dark mode theme with CSS variables?** Define your color tokens on :root, then override those same variables under a selector like :root[data-theme='dark'] or inside a prefers-color-scheme: dark media query. Because every component reads the variables instead of hard-coded hex values, flipping one attribute with JavaScript re-themes the entire page without touching any component styles. --- # A Complete Guide to Flexbox: Every Property, Explained > Flexbox is a one-dimensional CSS layout system for arranging items along a single axis. Set display: flex on a container and its children become flex items that line up in a row or column. Container properties like justify-content, align-items, and gap position the group; item properties like flex-grow, flex-shrink, and flex-basis (usually via the flex shorthand) decide how each item shares the space. Source: https://thesassway.com/flexbox-guide/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Flexbox in one sentence Flexbox is a one-dimensional CSS layout system that distributes space and aligns items along a single axis. You set `display: flex` on a **container**, and its direct children become **flex items** that line up in a row (or column) and can grow, shrink, and align to fill the available space. That is the whole idea: a parent that lays out its children along one axis, with fine-grained control over spacing and alignment. Everything else in Flexbox is just properties that adjust how that distribution and alignment behave. This guide walks through every one of them, grouped by whether it lives on the container or on the items, with copy-pasteable code and the mental model that makes each property click. ```css .container { display: flex; } ``` That single declaration turns children into flex items. Below we make it do real work. ## The two axes: main and cross Flexbox has no fixed "horizontal" and "vertical." Instead it has a **main axis** and a **cross axis**, and which direction they point depends on `flex-direction`. - The **main axis** runs in the direction items flow. With the default `flex-direction: row`, it runs left to right. - The **cross axis** runs perpendicular to it. With a row, the cross axis runs top to bottom. This matters because the alignment properties are named after axes, not directions. `justify-content` always works along the main axis; `align-items` always works along the cross axis. Switch `flex-direction` to `column` and their real-world effect swaps, because the axes rotated. Once you internalize main-vs-cross, the property names stop feeling arbitrary. | Concept | Default (`row`) direction | With `flex-direction: column` | | --- | --- | --- | | Main axis | Left → right | Top → bottom | | Cross axis | Top → bottom | Left → right | | `justify-content` controls | Horizontal spacing | Vertical spacing | | `align-items` controls | Vertical alignment | Horizontal alignment | ## Container properties These go on the element with `display: flex`. They govern how the whole group behaves. ### flex-direction Sets the main axis and the order items flow in. ```css .container { display: flex; flex-direction: row; /* row | row-reverse | column | column-reverse */ } ``` - `row` (default): items left to right. - `column`: items top to bottom — the single most common reason people reach for a column is stacking cards or form fields. - `row-reverse` / `column-reverse`: same axis, reversed order. Note this is a **visual** reversal only; it does not change DOM or tab order, so avoid it where reading order matters for accessibility. ### flex-wrap By default flex items refuse to wrap — they shrink to fit on one line instead. `flex-wrap` changes that. ```css .container { display: flex; flex-wrap: wrap; /* nowrap (default) | wrap | wrap-reverse */ } ``` Set `wrap` and items that no longer fit drop to a new line. This is the foundation of a simple responsive grid: give each item a `flex-basis` and let wrapping handle the rest, no media queries required. The shorthand `flex-flow: row wrap` combines `flex-direction` and `flex-wrap` in one line. ### justify-content Distributes leftover space along the **main axis**. ```css .container { display: flex; justify-content: space-between; } ``` | Value | What it does | | --- | --- | | `flex-start` | Items packed to the start (default) | | `flex-end` | Items packed to the end | | `center` | Items centered as a group | | `space-between` | First and last item touch the edges; equal gaps between | | `space-around` | Equal space around each item (edges get half-gaps) | | `space-evenly` | Truly equal gaps, including at the edges | `space-between` is the workhorse for navbars: logo hard left, links hard right, one line of CSS. ### align-items Aligns items along the **cross axis** — for a row, that is vertical alignment. ```css .container { display: flex; align-items: center; } ``` - `stretch` (default): items stretch to fill the container's cross size. This is why two side-by-side cards with different content end up the same height automatically. - `flex-start` / `flex-end`: align to the top / bottom of a row. - `center`: center vertically — the second half of the classic centering trick. - `baseline`: align items by their text baselines, useful when items have different font sizes. ### align-content Only does anything when items wrap onto **multiple lines**. It distributes space between the lines along the cross axis, using the same value set as `justify-content` (`flex-start`, `center`, `space-between`, and so on). On a single-line flex container it has no effect, which trips up a lot of people expecting it to behave like `align-items`. ### gap The cleanest way to space flex items. `gap` adds consistent spacing **between** items without adding it on the outer edges. ```css .container { display: flex; gap: 1rem; /* row-gap and column-gap at once */ /* gap: 1rem 2rem; row-gap column-gap */ } ``` Before `gap` was supported in Flexbox, people used margins and negative-margin hacks. Those days are over: `gap` works in every current browser (Chrome, Firefox, Safari, and Edge have supported flex `gap` since 2021). Reach for it instead of margins on items. ## Item properties These go on the flex items — the children — and control how each one behaves within the shared space. ### flex-grow How much an item grows to absorb **extra** free space, relative to its siblings. It is a unitless proportion, not a size. ```css .item { flex-grow: 1; /* default is 0 (do not grow) */ } ``` If every item has `flex-grow: 1`, they share leftover space equally. Give one item `flex-grow: 2` and it takes twice as large a share of the *extra* space (not twice the total width). A single `flex-grow: 1` on one child is the standard way to make it fill the remaining room — think a search input that expands while buttons stay their natural size. ### flex-shrink The mirror image: how much an item shrinks when there is **not enough** room. ```css .item { flex-shrink: 1; /* default is 1 (allowed to shrink) */ } ``` Default `1` means items shrink proportionally to avoid overflow. Set `flex-shrink: 0` to forbid an item from shrinking — handy for a fixed-width sidebar or an icon that must never squish. ### flex-basis The item's size along the main axis **before** grow and shrink are applied. It is the starting point. ```css .item { flex-basis: 200px; /* auto (default) | | */ } ``` `auto` means "use my `width`/`height` or my content size." Set `flex-basis: 0` together with `flex-grow: 1` on several items and they become perfectly equal columns, because they all start from zero and divide space evenly. ### The flex shorthand You will almost always set these three through the `flex` shorthand rather than individually. Learn these values and you have covered 95% of real usage. | Shorthand | Expands to | Meaning | | --- | --- | --- | | `flex: 1` | `1 1 0%` | Grow and shrink from a zero basis — equal-width columns | | `flex: auto` | `1 1 auto` | Grow and shrink from natural size | | `flex: none` | `0 0 auto` | Fixed at natural size, never grow or shrink | | `flex: 0 1 auto` | (the default) | Shrink but don't grow | | `flex: 1 1 200px` | `1 1 200px` | Start at 200px, then flex | ```css /* Three equal columns */ .column { flex: 1; } ``` ### align-self Overrides the container's `align-items` for a **single** item. Same value set (`auto`, `flex-start`, `flex-end`, `center`, `baseline`, `stretch`). Use it when one item needs to break from the group — say, pinning a "New" badge to the top of a row of centered items. ```css .special { align-self: flex-start; } ``` ### order Changes the **visual** position of an item without touching the HTML. Lower numbers come first; the default is `0`, so `order: -1` pulls an item to the front. ```css .promoted { order: -1; } ``` Use `order` sparingly. Like `row-reverse`, it changes visual order but not DOM or keyboard tab order, so heavy use creates a confusing, inaccessible experience for keyboard and screen-reader users. It shines for small, responsive rearrangements, not wholesale reordering. ## Two patterns you'll use constantly ### Perfect centering The reason Flexbox first won people over. Three lines center anything, horizontally and vertically, with no magic numbers: ```css .center { display: flex; justify-content: center; /* main axis */ align-items: center; /* cross axis */ } ``` This works regardless of the child's size. For a deeper tour of the alternatives and their trade-offs, see our guide on [how to center a div](/how-to-center-a-div/). ### A responsive card row with no media queries Combine `flex-wrap`, `gap`, and a `flex-basis` and the layout adapts on its own: ```css .cards { display: flex; flex-wrap: wrap; gap: 1rem; } .card { flex: 1 1 250px; /* grow, shrink, never narrower than ~250px */ } ``` Each card wants to be at least 250px wide. Three fit on a wide screen, two on a tablet, one on a phone — the browser recalculates as the viewport changes, and you never wrote a breakpoint. This is Flexbox at its best: intrinsically responsive. ## Flexbox in Sass If you use Sass, wrap common Flexbox patterns in a mixin so you stop repeating the same three lines everywhere: ```scss @mixin flex-center($direction: row) { display: flex; flex-direction: $direction; justify-content: center; align-items: center; } .hero { @include flex-center(column); gap: 1rem; } ``` That compiles to standard CSS but keeps your intent readable. If you are new to writing mixins, our [complete guide to Sass](/complete-guide-to-sass/) covers the syntax and when a mixin beats a plain class. ## When Flexbox is the wrong tool Flexbox is one-dimensional: it excels at laying out items along a single row or column. When you need to control rows **and** columns at the same time — a true two-dimensional grid where things line up both horizontally and vertically — CSS Grid is the better fit. A common, sensible approach is Grid for the overall page skeleton and Flexbox for the components inside each region. Read [Flexbox vs CSS Grid](/flexbox-vs-grid/) for a decision framework, and the step-by-step [CSS Grid tutorial](/css-grid-tutorial/) when you're ready to build a two-dimensional layout. ## Browser support and gotchas Flexbox is universally supported — every browser in current use has shipped unprefixed Flexbox for years, so you can use it in production without fallbacks or vendor prefixes. A few things still catch people out: - **`align-content` needs multiple lines.** On a single-line container it does nothing; you probably want `align-items`. - **`flex-shrink` can crush content.** If text or an image is being squished unexpectedly, set `flex-shrink: 0` on that item. - **Percentage `flex-basis` needs a sized parent** along the main axis to resolve against, or it falls back to content size. - **`order` and `*-reverse` don't move focus order.** They reorder pixels, not the DOM — keep that in mind for accessibility. - **Minimum size defaults to content.** A flex item won't shrink below its content's intrinsic minimum unless you add `min-width: 0` (or `min-height: 0` in a column). This is the fix for the infamous "my flex item overflows its container" bug. ## Quick reference cheat sheet | Property | Goes on | Controls | | --- | --- | --- | | `flex-direction` | Container | Direction of the main axis | | `flex-wrap` | Container | Whether items wrap to new lines | | `justify-content` | Container | Spacing along the main axis | | `align-items` | Container | Alignment along the cross axis | | `align-content` | Container | Spacing of wrapped lines (multi-line only) | | `gap` | Container | Space between items | | `flex-grow` | Item | Share of extra space | | `flex-shrink` | Item | How much it shrinks when cramped | | `flex-basis` | Item | Starting size on the main axis | | `flex` | Item | Shorthand for grow / shrink / basis | | `align-self` | Item | Override cross-axis alignment for one item | | `order` | Item | Visual position (not DOM order) | Master the two axes and the container-versus-item split, and the rest of Flexbox is just picking the property that names what you want. Set `display: flex`, decide your direction, then reach for `justify-content`, `align-items`, and `gap` to place things — and the `flex` shorthand to decide how items share the space. ## Frequently asked questions **What is the difference between justify-content and align-items?** They control different axes. justify-content distributes items along the main axis (horizontal by default, so it handles left-to-right spacing), while align-items aligns items along the cross axis (vertical by default). Switch flex-direction to column and their real-world directions swap, because the main and cross axes rotate. To center something both ways, set justify-content: center and align-items: center together. **How does flexbox work?** You set display: flex on a container, turning its direct children into flex items that flow along a main axis. The browser distributes free space and aligns items using container properties (flex-direction, justify-content, align-items, gap) and item properties (flex-grow, flex-shrink, flex-basis). Items can grow to fill extra room, shrink to avoid overflow, and wrap to new lines, which makes flexbox naturally responsive without media queries. **What does the flex: 1 shorthand mean?** flex: 1 expands to flex: 1 1 0%, meaning flex-grow: 1, flex-shrink: 1, and flex-basis: 0%. Because every item starts from a zero basis and grows equally, applying flex: 1 to several siblings produces perfectly equal-width columns. It is the most common flexbox shorthand. Use flex: auto to flex from natural content size, or flex: none for a fixed, non-flexing item. **When should I use flexbox instead of CSS Grid?** Use flexbox for one-dimensional layouts, arranging items in a single row or column such as navbars, button groups, and card rows. Use CSS Grid when you need to control rows and columns at the same time in a true two-dimensional layout. A common pattern is Grid for the overall page structure and flexbox for the components inside each region. They work well together, not as rivals. **Why is my flex item overflowing its container?** By default a flex item will not shrink below its content's intrinsic minimum size, so long text or wide images can push past the container. The fix is to add min-width: 0 (or min-height: 0 in a column) to the affected item, which lets it shrink properly. Setting flex-shrink: 0 does the opposite and prevents shrinking, so use min-width: 0 when you want the item to compress. **Do I still need vendor prefixes for flexbox?** No. Every browser in current use has shipped unprefixed flexbox for years, so you can write display: flex in production without -webkit- or -ms- prefixes or fallbacks. Older prefixed and legacy syntaxes only matter if you must support very old browsers like Internet Explorer 10, which is effectively gone. For modern projects, use the standard properties directly and reach for gap instead of margin hacks for spacing. --- # Flexbox vs CSS Grid: Which Layout Should You Actually Use? > Use Flexbox for one-dimensional layouts (a single row or column) like navbars, toolbars, and the inside of cards, where content drives the spacing. Use CSS Grid for two-dimensional layouts (rows and columns together) like page shells, galleries, and dashboards, where you define the structure first. Most real pages use Grid for the overall layout and Flexbox for the components inside it. Source: https://thesassway.com/flexbox-vs-grid/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## The short answer: one dimension vs two Reach for **Flexbox** when you're laying out items along a single line — a row of nav links, a card's inner stack, a toolbar that wraps. Reach for **CSS Grid** when you need to control **rows and columns at the same time** — a page shell, an image gallery, a dashboard. Flexbox is a **one-dimensional** layout system: it distributes space along one axis at a time (a row *or* a column). Grid is **two-dimensional**: it manages rows *and* columns together, so items line up in both directions without you nudging anything by hand. That single distinction resolves most "which one?" arguments. Everything below is the detail that makes it stick — plus the cases where the two genuinely overlap, and how they work together in real layouts. ## Flexbox vs Grid at a glance | | Flexbox | CSS Grid | |---|---|---| | **Dimensions** | One (row or column) | Two (rows and columns) | | **Layout driven by** | Content — items size to fit | Container — you define the tracks | | **Best for** | Components, nav bars, wrapping chips | Page layouts, galleries, dashboards | | **Alignment** | `justify-content` / `align-items` | Same, plus `justify-items` / place shorthands | | **Gaps** | `gap` (supported) | `gap` (built in from day one) | | **Overlap items** | Awkward | Easy (`grid-area` / z-index) | | **Browser support** | Universal | Universal (98%+ globally) | Both ship in every modern browser and have for years. Neither is "newer" in any way that should affect your choice today — pick on fit, not fear. ## The mental model that actually helps The cleanest way to decide is to ask: **is the content deciding the layout, or is the layout deciding the content?** - **Flexbox is content-first.** You drop items in and Flexbox distributes whatever space is left over. Items can grow, shrink, and wrap based on their own size. You rarely say "there are three columns" — you say "space these out evenly and let them wrap." - **Grid is layout-first.** You define the structure — "three equal columns, a header row, a sidebar" — and then place content into it. The grid exists before the content arrives. A navbar is content-first: you don't know how many links there are, you just want them in a row with space between. A magazine page is layout-first: the columns and regions are the whole point. ## When to use Flexbox Use Flexbox when your layout is essentially **a line of things** (that line may wrap onto multiple lines, but each line is handled one axis at a time): - Navigation bars and menus - Toolbars and button groups - The inside of a card (image, then title, then a footer pushed to the bottom) - Centering a single element - Tag/chip lists that wrap - Vertically distributing items in a sidebar Flexbox shines at **distributing leftover space** and at pushing things apart. The classic "logo left, links right" navbar is three lines of CSS: ```css .navbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } ``` `justify-content: space-between` pins the first item to the start and the last to the end, spreading the rest. `align-items: center` handles vertical alignment on the cross axis. Add `flex-wrap: wrap` and the row gracefully breaks onto a second line when space runs out — no media query required. Another Flexbox superpower is the **"push to the end"** trick inside a card: ```css .card { display: flex; flex-direction: column; } .card .cta { margin-top: auto; /* shoves the button to the bottom */ } ``` `margin-top: auto` eats all the free space above the element, pinning it to the bottom regardless of how much text sits above it. That's a genuinely hard thing to do cleanly without Flexbox. ## When to use CSS Grid Use Grid when you need **rows and columns to relate to each other** — when items should line up both across and down: - Overall page layout (header, sidebar, main, footer) - Image galleries and product grids - Dashboards and card layouts with equal-height rows - Any design where columns must stay aligned across rows - Forms with label/field columns that line up - Overlapping elements (a caption over an image, layered UI) The signature Grid move is the **responsive card grid with no media queries**: ```css .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; } ``` Read that middle line as: "make as many columns as fit, each at least 250px wide, sharing leftover space equally." On a phone you get one column; on a laptop, three or four — and the browser does the math. `auto-fit` collapses empty tracks so items stretch to fill the row; swap in `auto-fill` if you'd rather keep empty columns reserved. For a full page shell, **named areas** make the structure readable at a glance: ```css .layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header" "sidebar main" "footer footer"; min-height: 100vh; } .layout header { grid-area: header; } .layout .sidebar { grid-area: sidebar; } .layout main { grid-area: main; } .layout footer { grid-area: footer; } ``` You can practically *see* the layout in the `grid-template-areas` string. Rearranging the page is a matter of editing that ASCII map — no reordering markup. Our step-by-step [CSS Grid tutorial](/css-grid-tutorial/) builds this pattern out into a full responsive page. ## Head-to-head: the cases people actually argue about ### Centering one thing Both do it. Flexbox is the reflexive choice: ```css .center { display: flex; justify-content: center; align-items: center; } ``` Grid does it in fewer declarations with `place-items`: ```css .center { display: grid; place-items: center; } ``` For a single centered element, Grid's `place-items: center` is the tersest option in CSS. For centering *within a row of other flex items*, stick with Flexbox. ### Equal-height columns If you want three columns that are all as tall as the tallest one, **Grid does this for free** — every item in a row shares the row's height by default. In Flexbox, `align-items: stretch` (the default) gives equal-height flex items along a single row too, so both handle the simple case. Grid wins the moment you have *multiple rows* that must also align. ### Gaps between items Both support the `gap` property today. It landed in Grid first and arrived in Flexbox later, but every current browser supports `gap` in Flexbox now. You no longer need negative-margin hacks in either system. ### Wrapping vs reflowing - **Flexbox `flex-wrap: wrap`**: items flow onto new lines, but each new line sizes independently — items in row two don't align with items in row one. - **Grid**: items always snap to the same column tracks, so everything lines up in a strict matrix. If you want a ragged, content-driven wrap, that's Flexbox. If you want a tidy grid, that's Grid. This is the single clearest tell for which tool a design wants. ## They're not rivals — use both Here's the part that resolves the whole debate: **you almost never choose one for an entire site.** Real layouts nest them. A typical page uses Grid for the **macro** layout — the header/sidebar/main/footer skeleton — and Flexbox for the **micro** layout inside each region: the nav links in the header, the buttons in a card, the meta row under an article title. ```css /* Grid for the page shell */ .page { display: grid; grid-template-columns: 1fr 3fr; gap: 2rem; } /* Flexbox inside a single card */ .card { display: flex; flex-direction: column; gap: 0.75rem; } ``` A grid item can be a flex container, and a flex item can be a grid container. Nest as deep as the design needs. Learning both well — see our [complete guide to Flexbox](/flexbox-guide/) for every property — means you stop asking "which one?" and start asking "which one *here?*", region by region. ## Quick decision cheat-sheet | If you're building… | Use | |---|---| | A navbar or menu | Flexbox | | A row of buttons | Flexbox | | The inside of a card | Flexbox | | Centering one element | Either (`place-items: center` is shortest) | | A responsive card gallery | Grid (`auto-fit` + `minmax`) | | A full page layout | Grid (named areas) | | A dashboard with aligned rows | Grid | | Chips/tags that wrap raggedly | Flexbox | | Anything that must align in 2 directions | Grid | ## A note on performance and support There's no meaningful performance difference between the two for typical layouts — both are highly optimized native browser features, and neither will be your bottleneck. Choose on fit and readability, not speed. On support: both Flexbox and Grid work in every browser people realistically use, with global support well above 98%. Grid was the later arrival historically, which is why some older tutorials treat it as risky — that caution is outdated. If you're targeting Internet Explorer 11 (rare in 2026), note it used an older, incomplete Grid syntax and a slightly buggy early Flexbox; for anything else, both are safe to ship. As always, check current support for any specific sub-feature before you rely on it. ## The bottom line Ask one question: **is this layout a line, or a grid?** - **A line of items** — one axis, content-driven, may wrap → **Flexbox**. - **Rows and columns together** — two axes, layout-driven, aligned → **Grid**. Most pages want Grid for the overall structure and Flexbox for the components inside it. You're not picking a side for life; you're picking the right tool for each box. Once that clicks, layout stops being a fight. For the bigger picture on adapting these layouts across screen sizes, our [responsive web design guide](/responsive-design-guide/) ties Flexbox, Grid, and media queries together. ## Frequently asked questions **Is Flexbox or Grid better?** Neither is universally better; they solve different problems. Flexbox is best for one-dimensional layouts (items along a single row or column), while Grid is best for two-dimensional layouts where rows and columns must align together. Most well-built pages use both: Grid for the page structure and Flexbox for the smaller components nested inside it. **Should I use Flexbox or Grid for responsive design?** Both are responsive tools you'll usually combine. Grid's repeat(auto-fit, minmax(250px, 1fr)) builds a fluid card grid with no media queries, and Flexbox with flex-wrap: wrap reflows items when space runs out. Reach for Grid when columns must stay aligned across rows, and Flexbox when a single row of items just needs to wrap. **Can I use Flexbox and Grid together?** Yes, and you usually should. A grid item can be a flex container and a flex item can be a grid container, so they nest freely. The common pattern is Grid for the macro page layout (header, sidebar, main, footer) and Flexbox for the micro layout inside each region, like nav links or the buttons in a card. **Is CSS Grid harder to learn than Flexbox?** Grid has more properties and concepts (tracks, named areas, line placement), so it feels larger at first, but its layout-first model is arguably more intuitive for page structure. Flexbox has a smaller surface area but its growing, shrinking, and wrapping behavior can surprise beginners. Learn Flexbox for components first, then Grid for full layouts. **Which is better for centering a div, Flexbox or Grid?** Both center reliably. Grid is the shortest: display: grid with place-items: center centers a single element in two declarations. Flexbox needs justify-content: center and align-items: center. For centering one item, Grid wins on brevity; for centering something within a row of other flex items, stick with Flexbox. **Do Flexbox and Grid work in all browsers?** Yes. Both are supported in every modern browser, with global support well above 98 percent, and have been production-safe for years. The main exception is Internet Explorer 11, which used an older, incomplete Grid syntax and a buggy early Flexbox. For any current browser, both are safe to ship; check support for specific sub-features before relying on them. --- # How to Center a Div in CSS (5 Ways That Actually Work) > To center a div on both axes, set its parent to display: flex, justify-content: center, and align-items: center, then give the parent usable height such as min-height: 100vh. Use margin-inline: auto for a narrower block in normal flow, text-align: center for inline content, Grid place-items: center for grid items, and positioned transforms for overlays. Source: https://thesassway.com/how-to-center-a-div/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-09-07 ## The Fastest Way to Center a Div To center a `
` horizontally and vertically, give its parent three lines: `display: flex`, `justify-content: center`, and `align-items: center`. The parent also needs usable height if you want to see vertical movement. Flexbox is a sensible default for one item or one group, but the correct method depends on what “center” means in this layout. ```css .parent { display: flex; justify-content: center; align-items: center; } ``` There are no fixed child dimensions or negative margins here. Add `min-height: 100vh` when the parent should be at least as tall as the viewport. If the parent is only as tall as its contents, there is no spare vertical space to distribute. CSS is being literal, which is one of its less charming but more useful habits. ## What Exactly Are You Centering? “Center a div” can describe several different jobs. Choose by target and layout context before choosing a property. | Target | Use | Constraint to check | |---|---|---| | A narrower block, horizontally | `margin: 0 auto` | Its used width must leave inline space | | Text or an inline-level child | `text-align: center` on the parent | It does not center a block box | | Flex items as a group | `justify-content` and `align-items` | The parent needs free space on each axis | | Every grid item inside its grid area | `place-items: center` | Set it on the grid container | | One grid item inside its grid area | `place-self: center` | Set it on that item, not its siblings | | The grid tracks as a group | `place-content: center` | The grid must be smaller than its container | | An overlay removed from normal flow | Absolute positioning plus `transform` | Establish the intended containing block | That item-versus-group distinction is the useful part. `place-items`, `place-self`, and `place-content` sound like a committee named them during a long afternoon, but they control three different alignment targets. ## Method 1: `margin: 0 auto` in Normal Flow Use auto inline margins when a block should sit in the horizontal middle of its containing block. ```css .box { width: 600px; max-width: 100%; margin: 0 auto; } ``` The browser gives the left and right auto margins equal used values. The key condition is free inline space. An ordinary block with `width: auto` fills the available width, so its auto side margins resolve to zero and nothing appears to move. An explicit width is one way to create that space; the 600px width plus `max-width: 100%` above also keeps the box inside a narrower parent. This is a sizing rule, not a centering incantation. The [CSS box model](/css-box-model/) explains how content, padding, borders, and margins contribute to the result. The W3C’s [CSS 2.2 block-width rules](https://www.w3.org/TR/CSS22/visudet.html#blockwidth) specify both the `width: auto` behavior and the equal auto margins. In normal block flow, `margin: 0 auto` centers horizontally, not vertically. Flex and Grid change the context: their auto margins can absorb free space in the corresponding dimension. For example, this centers the child on both axes without adding alignment properties: ```css .parent { display: grid; min-height: 100vh; } .child { margin: auto; } ``` ## Method 2: `text-align: center` for Inline Content Use `text-align` for text, images, or an `inline` or `inline-block` element. Set it on the parent. ```css .parent { text-align: center; } ``` The property aligns inline-level content along the inline axis when that content does not fill the line box, as the [CSS Text specification](https://www.w3.org/TR/css-text-3/#text-align-property) defines it. It does not center a block-level child with a set width. For that block, use auto margins or change the layout context to Flexbox or Grid. Changing the child to `display: inline-block` makes it inline-level and therefore affected by the parent’s `text-align`. ## Method 3: Flexbox for Both Axes Flexbox is the practical default when you want to center one child or a row of children without removing them from normal flow. ```css .parent { display: flex; justify-content: center; align-items: center; min-height: 100vh; } ``` The [W3C Flexbox specification](https://www.w3.org/TR/css-flexbox-1/) defines `justify-content` on the main axis and `align-items` on the cross axis. With the default `flex-direction: row`, those normally map to horizontal and vertical. Set `flex-direction: column`, and the main axis becomes vertical, so the physical directions appear to swap. The properties did not swap jobs; the axis moved. Drop `align-items` when you only need main-axis centering. Drop `justify-content` when you only need cross-axis centering. In either case, the container needs leftover space on the relevant axis. A parent that hugs its child cannot visibly distribute space it does not have. For the full property map, use the [complete guide to Flexbox](/flexbox-guide/). To center several siblings as one row while keeping a 1rem gap: ```css .parent { display: flex; justify-content: center; align-items: center; gap: 1rem; } ``` ## Method 4: CSS Grid with `place-items` Grid offers the shortest two-axis recipe for children inside their grid areas. ```css .parent { display: grid; place-items: center; min-height: 100vh; } ``` The [CSS Box Alignment specification](https://www.w3.org/TR/css-align-3/) defines `place-items` as the shorthand for `align-items` and `justify-items`. It sets the default self-alignment behavior of the container’s child boxes. That makes it convenient for a dedicated hero, empty state, or loading-spinner container. For one grid child, leave the siblings alone and set the shorthand on that item: ```css .featured-item { place-self: center; } ``` `place-self` sets `align-self` and `justify-self` for that box. By contrast, `place-content: center` aligns the grid contents, meaning the track group, inside a container that has extra space. It does not mean “center each item.” The [CSS Grid tutorial](/css-grid-tutorial/) covers tracks and responsive layouts; this distinction is all you need for choosing the centering property. ## Method 5: Absolute Positioning Plus `transform` Use positioned centering when the element must sit over other content, such as a modal panel, tooltip, or badge, and its dimensions are unknown. ```css .parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } ``` `top: 50%` and `left: 50%` put the child’s top-left corner at the midpoint of its containing block. The negative translation moves the child back by half its own reference-box width and height. The [CSS Transforms specification](https://www.w3.org/TR/css-transforms-1/#transform-functions) defines horizontal translation percentages against the reference-box width and vertical percentages against its height. `position: relative` on the intended parent is the clearest way to establish the local positioning context. It is not the only property that can establish one. Per [CSS Positioned Layout](https://www.w3.org/TR/css-position-3/#def-cb), an absolutely positioned box uses the nearest ancestor that establishes an absolute-position containing block; `position`, `transform`, `will-change`, and `contain` are among the properties that can do that. If an element jumps somewhere unexpected, inspect its containing block rather than automatically blaming the transform. ## Bonus: A Reusable Sass Centering Mixin If the same Flexbox pattern appears throughout a project, a Sass mixin can package it without changing the generated CSS behavior. The official [Sass mixin documentation](https://sass-lang.com/documentation/at-rules/mixin/) describes mixins as reusable groups of styles, while the [`@if` documentation](https://sass-lang.com/documentation/at-rules/control/if/) defines the conditional branches used here. ```scss @mixin center($direction: both) { display: flex; @if $direction == both { justify-content: center; align-items: center; } @else if $direction == horizontal { justify-content: center; } @else if $direction == vertical { align-items: center; } } .hero { min-height: 100vh; @include center; } .toolbar { @include center(horizontal); } ``` The `both`, `horizontal`, and `vertical` arguments keep the call sites explicit. The mixin is useful when it represents a project convention; for a single selector, the three declarations are clearer than an abstraction. ## Why Is the Div Still Not Centered? Check the constraint before changing the method: - **Vertical centering looks unchanged:** the parent has no extra height. Add an appropriate `min-height`, fixed height, or inherited height. - **Auto margins look unchanged:** in normal flow, the block’s used width consumes all available inline space. Preserve a narrower used width. - **The wrong flex direction moves:** `flex-direction` changed the main axis. `justify-content` follows that axis; `align-items` follows the cross axis. - **Every grid item moved:** `place-items` was set on the container. Use `place-self` on the one item that should move. - **The tracks moved but items still stretch:** `place-content` aligned the grid contents. Use `place-items` or `place-self` for item alignment. - **The positioned child jumps outside its parent:** another ancestor establishes the containing block, or the intended parent does not. Inspect the ancestor chain. - **A full-width child causes horizontal overflow:** `100vw` follows the viewport, not the child’s containing block, and viewport units are generally sized as though scrollbars do not exist. If the child should follow its container, prefer `width: 100%`. The exact scrollbar exception depends on root `overflow` or `scrollbar-gutter`, as defined in [CSS Values and Units](https://www.w3.org/TR/css-values-4/#viewport-relative-lengths). ## Which Method Should You Use? Use this order: 1. For one item or one group on both Flexbox axes, use `display: flex`, `justify-content: center`, and `align-items: center`. 2. For grid items inside their areas, use `place-items: center`; for one item, use `place-self: center`. 3. For a narrower normal-flow block horizontally, use `margin: 0 auto` with a used width that leaves free space. 4. For text or inline-level content, use `text-align: center` on the parent. 5. For an overlay of unknown size, use absolute positioning with `translate(-50%, -50%)` and verify its containing block. The method follows the alignment target, the layout mode, and the available space. Once those three facts are explicit, centering stops being a bag of tricks and becomes a short diagnosis. ## Sources - [W3C, CSS Flexible Box Layout Module Level 1](https://www.w3.org/TR/css-flexbox-1/) — main-axis, cross-axis, alignment, and flex auto-margin behavior (accessed September 3, 2026). - [W3C, CSS Box Alignment Module Level 3](https://www.w3.org/TR/css-align-3/) — `place-items`, `place-self`, and `place-content` semantics (accessed September 3, 2026). - [W3C, CSS Grid Layout Module Level 2](https://www.w3.org/TR/css-grid-2/) — grid-item alignment and auto margins in Grid (accessed September 3, 2026). - [W3C, CSS 2.2 Visual Formatting Model Details](https://www.w3.org/TR/CSS22/visudet.html#blockwidth) — block widths and equal horizontal auto margins (accessed September 3, 2026). - [W3C, CSS Positioned Layout Module Level 3](https://www.w3.org/TR/css-position-3/) and [CSS Transforms Module Level 1](https://www.w3.org/TR/css-transforms-1/) — absolute-position containing blocks and percentage translations (accessed September 3, 2026). - [W3C, CSS Text Module Level 3](https://www.w3.org/TR/css-text-3/) and [CSS Values and Units Module Level 4](https://www.w3.org/TR/css-values-4/) — inline-content alignment and viewport-relative units (accessed September 3, 2026). - [Sass, `@mixin` and `@include`](https://sass-lang.com/documentation/at-rules/mixin/) and [`@if` and `@else`](https://sass-lang.com/documentation/at-rules/control/if/) — reusable style groups and conditional evaluation (accessed September 3, 2026). ## Frequently asked questions **How do I center a div horizontally and vertically?** Set the parent to display: flex, then add justify-content: center and align-items: center. Give the parent usable height, such as min-height: 100vh, so it has vertical space to distribute. CSS Grid with place-items: center is another two-axis option when the child is a grid item. **Why is my div not centering vertically?** The parent often has no extra height. Flexbox and Grid distribute available space, so a parent that is only as tall as its content shows no vertical movement. Give it an appropriate min-height, fixed height, or inherited height. Also check flex-direction, because it determines which physical direction is the main axis. **How do I center a div with margin auto?** In normal block flow, use margin: 0 auto and make sure the div’s used width is narrower than its containing block. Both horizontal auto margins then receive equal values. A width:auto block normally fills the available width, leaving no free space. In Flexbox or Grid, auto margins can also absorb free space vertically. **Should I use place-items, place-self, or place-content?** Use place-items: center on a grid container to set the default alignment of all its child items within their grid areas. Use place-self: center on one grid item without moving its siblings. Use place-content: center when the sized grid track group itself should sit in the center of a larger container. **How do I center an absolutely positioned div?** Set the child to position: absolute with top: 50%, left: 50%, and transform: translate(-50%, -50%). Set position: relative on the intended parent to establish a predictable containing block. The offsets place the child’s top-left corner at center, and the translation moves it back by half its own reference-box width and height. **What is the difference between justify-content and align-items?** In Flexbox, justify-content aligns items along the main axis, while align-items sets their default alignment on the cross axis. With flex-direction: row, those axes normally map to horizontal and vertical. With flex-direction: column, the main axis becomes vertical, so the properties appear to switch physical directions even though their roles stay the same. --- # How to Install and Compile Sass (npm, CLI, and Dart Sass) > Install Sass with npm by running npm install -D sass in your project, which gives you Dart Sass, the official implementation. Compile a file with npx sass input.scss output.css, or add --watch to recompile on every save. Avoid the deprecated node-sass package. You can also install globally with npm install -g sass, or via Homebrew or Chocolatey. Source: https://thesassway.com/how-to-install-sass/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Install Sass in one line, then compile with one command The fastest way to install Sass is with npm: run `npm install -D sass` inside your project, then compile with `npx sass input.scss output.css`. That gives you **Dart Sass**, the official and only actively maintained implementation, and it works the same on macOS, Windows, and Linux. Everything else in this guide — the CLI flags, the watch mode, the global install, and the build-tool setups — is just refinement on top of those two commands. If you only remember two lines, remember these: ```bash npm install -D sass npx sass scss/main.scss css/main.css ``` The rest of this guide explains the options so you can pick the install method that fits your project and avoid the outdated versions that still show up in old tutorials. ## Which Sass should you install? There have been three implementations of Sass over the years, and this trips up beginners because search results are full of dead advice. Here is the current state: | Implementation | Language | Status | Should you use it? | | --- | --- | --- | --- | | **Dart Sass** | Dart | Actively maintained, the reference implementation | Yes — this is "Sass" today | | LibSass (`node-sass`) | C++ | Deprecated, no new features | No — migrate off it | | Ruby Sass | Ruby | End-of-life since 2019 | No | The practical takeaway: **install `sass`, not `node-sass`.** The `node-sass` package wraps LibSass, which was officially deprecated and no longer receives new Sass features like the modern `@use` and `@forward` module system. If you inherit a project using `node-sass`, plan to swap it for `sass` — the npm package name is the main thing that changes. For a refresher on what the preprocessor actually does before you install it, see [what is Sass](/what-is-sass/). ## Method 1: Install Sass with npm (recommended) If your project already has Node.js and a `package.json`, npm is the cleanest option. It pins the Sass version per project, so every teammate and your CI server compile with the exact same compiler. ### Step 1 — Make sure Node.js is installed Sass's npm package needs Node. Check whether you have it: ```bash node -v npm -v ``` If those print version numbers (Node 14 or newer is fine; any recent LTS is ideal), you're set. If not, install Node.js from nodejs.org first — the LTS build is the safe choice. ### Step 2 — Install Sass as a dev dependency Inside your project folder: ```bash npm install --save-dev sass ``` The `--save-dev` flag (shortened to `-D`) records Sass under `devDependencies` in `package.json`, which is correct because Sass is a build tool, not something your shipped site needs at runtime. ### Step 3 — Compile a file Use `npx` to run the locally installed binary: ```bash npx sass scss/main.scss css/main.css ``` `npx` finds the `sass` executable inside `node_modules/.bin` so you don't need a global install. The first path is your source, the second is the CSS file Sass writes. ### Step 4 — Add npm scripts Typing that path every time gets old. Add scripts to `package.json` so the commands are short and self-documenting: ```json { "scripts": { "sass": "sass scss/main.scss css/main.css", "sass:watch": "sass --watch scss/main.scss css/main.css" } } ``` Now `npm run sass` compiles once and `npm run sass:watch` recompiles automatically on every save. ## Method 2: Install Sass globally with the CLI If you want a `sass` command available everywhere — handy for quick experiments or projects without a Node setup — install it globally: ```bash npm install -g sass ``` Then use it directly, no `npx`: ```bash sass input.scss output.css sass --watch input.scss output.css ``` There are also standalone options that don't require Node at all. On macOS you can use Homebrew: ```bash brew install sass/sass/sass ``` On Windows, Chocolatey works: ```bash choco install sass ``` The Dart Sass team also publishes standalone archives on the GitHub releases page for people who want a single executable with no package manager. All of these give you the same Dart Sass CLI. ### npm vs global install: which to choose? | Consideration | Local (`-D`) | Global (`-g`) | | --- | --- | --- | | Version pinned per project | Yes | No — one shared version | | Reproducible builds / CI | Yes | Not reliably | | Available in any folder | Only via `npx` | Yes, everywhere | | Best for | Real projects, teams | Quick tests, learning | For anything you'll maintain or ship, prefer the **local install**. Reach for global only when you want a throwaway `sass` command. ## Using watch mode and multiple files Watch mode is what makes the day-to-day workflow pleasant. Instead of compiling by hand, Sass listens for changes: ```bash sass --watch scss:css ``` The `scss:css` syntax compiles every `.scss` file in the `scss` folder into a matching file in the `css` folder — no need to name each file. This folder-to-folder mapping is the most common setup for a real project. ### Useful CLI flags | Flag | What it does | | --- | --- | | `--watch` | Recompile automatically when files change | | `--style=compressed` | Minify output for production | | `--no-source-map` | Skip the `.map` file | | `--load-path=` | Add a directory to the `@use`/`@import` search path | | `--help` | List every option | For a production build, compress the output: ```bash sass scss/main.scss css/main.css --style=compressed --no-source-map ``` By default Sass emits an expanded, readable stylesheet plus a source map, which is exactly what you want during development. ## Method 3: Let your build tool handle Sass Most modern front-end tooling can compile Sass for you, so you often don't run the CLI directly at all. In these setups you still install the `sass` package — the bundler calls it under the hood. - **Vite**: Install `sass` and Vite compiles any `.scss` file you import automatically. No config needed for a basic setup — just `import './styles/main.scss'`. - **webpack**: Add `sass-loader` alongside `sass`, `css-loader`, and `style-loader` in your loader chain. - **Parcel**: Detects `.scss` imports and compiles them with zero configuration once `sass` is installed. The rule of thumb: if you already use a bundler, install `sass` as a dev dependency and let the bundler drive it. Save the standalone CLI for static sites or plain HTML projects without a build step. ## Verify your install and compile your first file Confirm the compiler is working: ```bash sass --version ``` You should see a version number (Dart Sass releases are numbered like `1.x.x`). Then create a tiny test file, `scss/main.scss`: ```scss $brand: #2f6f4f; .button { padding: 0.75rem 1.25rem; background: $brand; border-radius: 6px; &:hover { background: darken($brand, 8%); } } ``` Compile it: ```bash npx sass scss/main.scss css/main.css ``` Open `css/main.css` and you'll see the nested rule flattened into standard CSS with the variable substituted in. That confirms the whole pipeline works. > Note: `darken()` and the other global color functions still work, but modern Sass recommends the `color.adjust()` and `color.scale()` functions from the built-in `sass:color` module. The one-liner above is fine for a smoke test; reach for the module functions in real code. ## Common install problems - **`command not found: sass`** — You likely installed locally, not globally. Use `npx sass ...`, or add an npm script. - **You accidentally installed `node-sass`** — Uninstall it (`npm uninstall node-sass`) and install `sass` instead. They are not interchangeable for modern syntax. - **`@use` rules throw errors** — You're probably on an old LibSass-based setup. The module system requires Dart Sass. - **Permission errors on a global install** — Avoid `sudo`; fix your npm prefix or just use a local install, which sidesteps the problem entirely. ## Where to go next Once Sass compiles, the payoff is in the features: variables, nesting, partials, mixins, and the module system. Work through [the complete guide to Sass](/complete-guide-to-sass/) to put your new setup to use, and if you're weighing your whole toolchain, compare the options in [CSS preprocessors vs PostCSS](/css-preprocessors-vs-postcss/). You can also browse more setup and build articles in [Tooling & Workflow](/tooling-and-workflow/). The short version stays true no matter how deep you go: `npm install -D sass`, point the CLI at your source file, and add `--watch` while you work. ## Frequently asked questions **How do I install Sass with npm?** Run npm install --save-dev sass inside your project folder. This installs Dart Sass as a dev dependency and records it in package.json. Compile with npx sass scss/main.scss css/main.css, or add npm scripts so you can run npm run sass. The --save-dev flag is correct because Sass is a build tool, not a runtime dependency your shipped site needs. **What is the difference between sass and node-sass?** The sass package is Dart Sass, the official and actively maintained implementation with the modern @use and @forward module system. node-sass wraps the deprecated LibSass engine, which no longer receives new Sass features. Always install sass for new projects, and migrate existing node-sass projects to sass to keep using current syntax. **How do I compile SCSS to CSS?** Point the Sass CLI at your source and output files: sass input.scss output.css. Add --watch to recompile automatically on save, and use folder syntax like sass --watch scss:css to compile a whole directory. For production, add --style=compressed to minify the output. With a bundler like Vite or webpack, importing an .scss file compiles it for you. **Do I need Node.js to install Sass?** Not necessarily. The npm methods (npm install sass) need Node.js, but Dart Sass also ships as a standalone executable. Install it with Homebrew on macOS (brew install sass/sass/sass), Chocolatey on Windows (choco install sass), or by downloading a standalone archive from the Dart Sass GitHub releases page. All give you the same sass command. **How do I install Sass globally?** Run npm install -g sass to make the sass command available in any folder without npx. Global installs are convenient for quick experiments and learning, but for real projects prefer a local dev dependency so the Sass version is pinned per project and your builds stay reproducible across teammates and CI. **How do I check if Sass is installed?** Run sass --version in your terminal. Dart Sass prints a version number like 1.x.x. If you installed locally with npm, use npx sass --version instead, since the binary lives in node_modules and is not on your global PATH. A command not found error usually means you installed locally and should use npx. --- # Mobile-First CSS: Why It Matters and How to Do It Right > Mobile-first CSS means writing your base styles for the smallest screen first, then using min-width media queries to progressively add complexity as the viewport grows. Instead of building a desktop layout and stripping it down for phones, you start with a simple single-column design that works everywhere and layer on multi-column layouts, larger type, and richer spacing only when the screen has room for them. Source: https://thesassway.com/mobile-first-css/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Mobile-First CSS in One Sentence **Mobile-first CSS means you write your base styles for the smallest screen first, then use `min-width` media queries to progressively add complexity as the viewport grows.** Instead of designing for a desktop and stripping things away for phones, you start with a single, simple column that works everywhere and layer on multi-column layouts, larger type, and richer spacing only when there's room for them. It's a mindset as much as a technique. The base stylesheet — everything outside a media query — is your phone layout. Every `@media (min-width: ...)` block is an enhancement, not a fix. ```css /* Base: mobile. No media query needed. */ .card-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; } /* Enhancement: tablets and up get two columns */ @media (min-width: 48em) { .card-grid { grid-template-columns: 1fr 1fr; } } /* Enhancement: wide screens get three */ @media (min-width: 64em) { .card-grid { grid-template-columns: repeat(3, 1fr); } } ``` Read top to bottom, that's the whole philosophy: sensible defaults, then additions. ## Why Mobile-First Beats Desktop-First The two approaches produce the same visual result if you're careful. But mobile-first wins on the things that actually matter in production: performance, maintainability, and the direction the cascade flows. ### Mobile devices are the majority of traffic For most public-facing sites, more than half of all visits come from phones. Starting with the phone layout means your default, no-media-query stylesheet is the one most of your users receive. That's the code path you want to be leanest and most reliable. ### The cascade flows the right way This is the technical heart of it. In mobile-first, you use `min-width` queries that add styles as screens get bigger. Each breakpoint only ever *adds* to what came before, so overrides are rare and the cascade reads like a story: small, then bigger, then biggest. Desktop-first inverts this. You write full desktop styles as your base, then use `max-width` queries to claw things back for smaller screens. You end up writing rules whose only job is to undo earlier rules — resetting a three-column grid back to one column, shrinking padding you already set, hiding a sidebar you already built. More overrides mean more specificity battles and more places for a bug to hide. ### Progressive enhancement, not graceful degradation A phone loading a mobile-first stylesheet can stop reading at the first media query and still get a complete, usable layout. Everything above the first `@media` block is a finished experience. That aligns with how CSS is meant to work: a solid baseline that richer environments enhance. | | Mobile-first | Desktop-first | |---|---|---| | Base styles target | Smallest screen | Largest screen | | Media query type | `min-width` | `max-width` | | Breakpoints mostly... | Add features | Remove/undo features | | Override frequency | Low | High | | Default (no-query) layout | Phone | Desktop | | Reads like | Small → large | Large → small | Neither approach is "wrong," and you'll meet real codebases built both ways. But mobile-first is the modern default because it produces less code that fights itself. ## How to Write Mobile-First Media Queries The mechanics are simple once you internalize one rule: **your base styles live outside any media query, and you only reach for `min-width`.** ### Start with the base Write everything the phone needs with no media query at all. Single column, full-width elements, comfortable tap targets, readable type. ```css .nav { display: flex; flex-direction: column; gap: 0.5rem; } .hero { padding: 2rem 1rem; font-size: 1.5rem; } ``` ### Add breakpoints upward Then enhance. Notice every query below is `min-width`, and each one only changes what needs to change. ```css @media (min-width: 48em) { .nav { flex-direction: row; /* horizontal nav once there's width */ } .hero { padding: 4rem 2rem; font-size: 2.5rem; } } ``` If you want to understand the query syntax itself in depth — `and`, ranges, `prefers-*` features — see our guide to [CSS media queries](/css-media-queries/). This article stays focused on the mobile-first *strategy* that sits on top of it. ### Order matters Because equal-specificity rules resolve by source order, write your `min-width` blocks from smallest to largest. A `64em` block placed above a `48em` block would let the smaller query override the larger one on wide screens — the opposite of what you want. ## Choosing Breakpoints (Without Guessing Device Widths) The most common mobile-first mistake is picking breakpoints to match specific phones or laptops. Don't. Device dimensions change every year, and you'll never keep up. **Set breakpoints where your content breaks, not where a device sits.** Widen your browser slowly. The moment a line of text gets uncomfortably long, or a card grid starts looking too sparse, that's a breakpoint. It's content-driven, and it ages well. That said, having a sane starting set helps. These are common, framework-agnostic values: | Label | Width (em) | Width (px @16) | Typical target | |---|---|---|---| | (base) | — | — | Phones, portrait | | sm | 36em | 576px | Large phones | | md | 48em | 768px | Tablets | | lg | 64em | 1024px | Small laptops | | xl | 80em | 1280px | Desktops | ### Why `em` for breakpoints Using `em` in the media query itself means breakpoints respond to the user's browser font-size setting. A reader who bumps their default font up gets your layout switching a little earlier, which keeps text from crowding. `1em` in a media query always equals the browser's root font size (usually 16px), regardless of any `font-size` you've set on elements — so `48em` reliably means 768px at default settings. It's a small accessibility win with no real downside. For the difference between `em`, `rem`, `px`, and the rest — and when each is the right call — our [CSS units guide](/css-units-guide/) breaks it down with examples. ## Mobile-First in Sass Sass makes mobile-first noticeably cleaner because you can name your breakpoints once and stop memorizing pixel values. A small mixin plus a map of tokens is the common pattern. ```scss // _breakpoints.scss $breakpoints: ( "sm": 36em, "md": 48em, "lg": 64em, "xl": 80em, ); @use "sass:map"; @mixin from($name) { $width: map.get($breakpoints, $name); @if $width { @media (min-width: $width) { @content; } } @else { @error "Unknown breakpoint: #{$name}."; } } ``` Now your components read almost like plain English, and the mobile-first order is baked in — base styles first, `@include from(...)` enhancements after: ```scss .card-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; @include from("md") { grid-template-columns: 1fr 1fr; } @include from("lg") { grid-template-columns: repeat(3, 1fr); } } ``` The `@media` rules bubble up to the top level when compiled, so nesting them inside the selector like this is just for readability — the output is identical to hand-written queries. This uses the modern `@use` and `sass:map` module syntax that ships with Dart Sass; the older `@import` and global `map-get()` still work but are on the way out. ## Layout Techniques That Are Mobile-First by Default Here's the part many tutorials bury: modern CSS layout often adapts *without* media queries at all. Lean on these and you'll write fewer breakpoints. - **Flexbox with `flex-wrap: wrap`** lets items sit in a row when there's space and stack when there isn't — no query required. - **`grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr))`** creates a grid that fits as many columns as will hold a 16rem minimum, then reflows down to one on narrow screens. This single line replaces three or four breakpoints of grid changes. - **Fluid type with `clamp()`** scales font size smoothly between a floor and a ceiling: `font-size: clamp(1.5rem, 4vw, 2.5rem);`. The text grows with the viewport instead of jumping at each breakpoint. ```css /* An intrinsically responsive grid — zero media queries */ .auto-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); } ``` Media queries are still essential for structural changes — turning a stacked nav into a horizontal bar, showing or hiding a sidebar — but treat them as the tool of last resort, after fluid units and intrinsic layout have done what they can. ## Common Mobile-First Mistakes Even with the right structure, a few habits quietly undo the benefits. - **Setting a fixed base width.** If your base `.container` is `width: 960px`, you're not mobile-first — you've hidden a desktop layout in the default styles and forced phones to scroll sideways. Base widths should be fluid (`width: 100%`, `max-width` for the ceiling). - **Forgetting the viewport meta tag.** No amount of mobile-first CSS matters if the page ships without ``. Without it, mobile browsers pretend to be ~980px wide and zoom out. - **Mixing `min-width` and `max-width` carelessly.** Combining both in one project is fine, but stacking overlapping ranges on the same property is how you get styles that apply in a band you didn't intend. Pick `min-width` as your default and stay consistent. - **Testing only in a desktop devtools emulator.** Emulators are great for iteration, but real phones expose touch-target size, font rendering, and scroll behavior that a resized browser window won't. Check on an actual device before shipping. - **Ignoring tap-target size.** Buttons and links need roughly 44×44px of hittable area on touch screens. That's a mobile-first concern your base styles should handle from the start. For where mobile-first fits into the bigger picture — images, performance, testing across a full range of devices — see our [responsive web design guide](/responsive-design-guide/), which uses this approach as its foundation. ## The Bottom Line Mobile-first CSS isn't a framework or a plugin — it's an ordering discipline. Write the phone layout as your base with no media query, use `min-width` to enhance upward, choose breakpoints where your content demands them, and lean on `clamp()`, `auto-fit`, and `flex-wrap` so you write fewer queries in the first place. Do that and your stylesheet stays small, your cascade stays sane, and the majority of your users — the ones on phones — get your fastest, most reliable code path by default. ## Frequently asked questions **What is mobile-first design?** Mobile-first design is an approach where you build the smallest-screen experience first, then enhance it for larger screens. In CSS, that means your base styles (outside any media query) target phones, and each min-width media query adds features as the viewport grows. It prioritizes the phone layout because mobile devices are the majority of web traffic for most sites. **What is the difference between mobile-first and desktop-first CSS?** Mobile-first starts with phone styles as the base and uses min-width queries to add complexity upward. Desktop-first starts with the full desktop layout and uses max-width queries to remove or undo features for smaller screens. Mobile-first tends to produce cleaner code because breakpoints add styles rather than overriding earlier ones, which means fewer specificity conflicts. **Should I use min-width or max-width media queries?** Use min-width for mobile-first CSS. Write your base styles for phones with no media query, then use min-width blocks ordered smallest to largest to enhance for bigger screens. Max-width is the desktop-first pattern. Pick one direction and stay consistent across a project; mixing overlapping min-width and max-width ranges on the same property causes styles to apply in bands you didn't intend. **What breakpoints should I use for mobile-first CSS?** Set breakpoints where your content breaks, not at specific device widths. Widen the browser until a layout looks cramped or a line gets too long, then add a breakpoint there. Common starting values are 36em (576px), 48em (768px), 64em (1024px), and 80em (1280px). Using em units keeps breakpoints responsive to the reader's browser font-size setting. **Do I still need media queries with modern CSS?** Fewer than before. Techniques like flex-wrap, grid with repeat(auto-fit, minmax()), and fluid type using clamp() adapt to screen size without any media query. Reach for those first. Media queries are still needed for structural changes, such as turning a stacked navigation into a horizontal bar or showing a sidebar, but treat them as a tool of last resort. **Why use em instead of px for media query breakpoints?** Breakpoints written in em respond to the browser's root font size, so a reader who increases their default text size gets your layout switching slightly earlier, keeping content from crowding. In a media query, 1em always equals the browser's root font size (usually 16px) regardless of element font sizes, so 48em reliably means 768px at default settings. It's a small accessibility win with no downside. --- # Responsive Web Design: The Complete Guide for 2026 > Responsive web design is the practice of building one website that adapts to any screen size, from small phones to large desktops, instead of maintaining separate mobile and desktop sites. It combines three core techniques: fluid layouts built with relative units, flexible images that never overflow their container, and CSS media queries (or container queries) that adjust the design at defined breakpoints. Source: https://thesassway.com/responsive-design-guide/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## What Responsive Web Design Actually Means Responsive web design is the practice of building **one** website that adapts its layout, type, and images to any screen, from a 320px phone to a 4K monitor, instead of shipping separate desktop and mobile sites. You do it with three core techniques working together: fluid layouts that use relative units, flexible images that never overflow their container, and CSS media queries (or container queries) that adjust the design at defined breakpoints. The goal is not to make a page "look good on mobile." The goal is a single codebase that reads well, stays usable, and performs fast on whatever device shows up. In 2026 that includes foldables, ultrawides, tablets held either orientation, and browsers where the user has bumped the default font size to 24px. A responsive site treats all of those as first-class, not edge cases. Here is the shortest possible version of responsiveness that actually works: ```css /* 1. Make the browser respect the viewport (in your HTML ) */ /* */ /* 2. Never let images overflow */ img { max-width: 100%; height: auto; } /* 3. Use a fluid, mobile-first layout */ .container { width: min(100% - 2rem, 65rem); margin-inline: auto; } ``` Those few lines already handle more edge cases than most hand-rolled "mobile stylesheets" from a decade ago. The rest of this guide explains why each piece matters and how to build up from here. ## Start With the Viewport Meta Tag Every responsive project begins with one line in the HTML ``: ```html ``` Without it, mobile browsers assume your page was designed for a ~980px desktop canvas and zoom the whole thing out, making text unreadable. `width=device-width` tells the browser to match the layout viewport to the device's actual width, and `initial-scale=1` sets the starting zoom to 100%. Do **not** add `maximum-scale=1` or `user-scalable=no`. Locking zoom breaks accessibility for anyone who needs to pinch-zoom to read, and it fails WCAG. If your layout only "works" when zoom is disabled, the layout is the problem. ## The Three Pillars of a Fluid Layout ### 1. Relative Units Over Fixed Pixels Fixed widths are the root cause of most broken mobile layouts. A `width: 960px` sidebar is fine on a laptop and a disaster on a phone. Reach for relative and intrinsic units instead. | Unit | What it's relative to | Best used for | |------|----------------------|---------------| | `%` | The parent element | Fluid column widths | | `rem` | The root font size | Spacing, type, breakpoints | | `em` | The element's own font size | Component-local spacing | | `vw` / `vh` | Viewport width / height | Full-bleed sections, hero height | | `ch` | Width of the "0" glyph | Readable line length (~60ch) | | `fr` | A fraction of free grid space | Grid column tracks | A practical rule: size **spacing and typography in `rem`**, size **layout tracks in `fr` or `%`**, and cap widths with `max-width` so content never stretches into an unreadable line. For a deeper breakdown of when each unit wins, see our guide to [CSS units](/css-units-guide/). ### 2. Modern CSS Functions Replace Most Media Queries Much of what used to require a media query is now handled by intrinsic sizing functions. These are supported in every current browser. ```css /* Clamp a value between a min and max, fluid in between */ .hero-title { font-size: clamp(1.75rem, 1rem + 4vw, 3.5rem); } /* Pick the smaller of two values — a fluid width with a hard cap */ .container { width: min(100% - 2rem, 65rem); margin-inline: auto; } /* A grid that reflows from 1 to N columns with NO media queries */ .card-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); } ``` That `auto-fit` / `minmax` grid is the single highest-leverage responsive pattern in modern CSS. Each card wants to be at least `18rem` wide; the browser fits as many columns as will comfortably fit and wraps the rest. The inner `min(100%, 18rem)` prevents overflow on screens narrower than 18rem. No breakpoints to maintain. ### 3. Flexbox and Grid Do the Heavy Lifting Flexbox and Grid are the layout engines behind responsive design. Flexbox excels at one-dimensional flows (a nav bar, a row of tags) and Grid at two-dimensional structure (page templates, card galleries). Knowing which to reach for saves hours; our [Flexbox vs Grid comparison](/flexbox-vs-grid/) walks through the decision. As a starting heuristic: if you're aligning items along a single axis, use Flexbox; if you're controlling rows *and* columns at once, use Grid. ## Media Queries: The Adjustment Layer When intrinsic functions can't express a change, media queries handle it. The classic pattern targets viewport width: ```css /* Base styles (mobile) apply everywhere */ .layout { display: grid; gap: 1rem; grid-template-columns: 1fr; } /* At 48rem (~768px) and up, add a sidebar */ @media (min-width: 48rem) { .layout { grid-template-columns: 16rem 1fr; } } ``` Note the units: use **`rem` in breakpoints, not `px`**. When a user increases their browser's default font size, `rem`-based breakpoints scale with them, so your layout switches to a roomier arrangement exactly when the larger text needs it. `px` breakpoints ignore that preference entirely. Media queries do far more than width. A few worth knowing in 2026: ```css /* Respect users who reduce motion */ @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } } /* Adapt to dark mode */ @media (prefers-color-scheme: dark) { :root { --bg: #111; --fg: #eee; } } /* Detect a coarse pointer (touch) and enlarge tap targets */ @media (pointer: coarse) { .btn { min-height: 44px; } } ``` Media queries are deep enough for their own article; our [CSS media queries guide](/css-media-queries/) covers ranges, `and`/`or` logic, and the newer `@media (width >= 48rem)` range syntax. ### Choosing Breakpoints Do not chase device names. The iPhone-15-this, Galaxy-that list changes every year and you will never keep up. Instead, **let the content decide**: resize the browser slowly and add a breakpoint wherever the layout starts to look cramped or the line length gets awkward. That said, a sane starting set covers most projects: | Breakpoint | Rough width | Typical target | |-----------|-------------|----------------| | Base | 0–479px | Small phones (no query, default) | | `30rem` | ~480px | Large phones | | `48rem` | ~768px | Tablets, small laptops | | `64rem` | ~1024px | Laptops, desktops | | `80rem` | ~1280px | Large desktops | Treat these as suggestions, not law. A data table might need a breakpoint at 52rem; a photo gallery might need none at all thanks to `auto-fit`. ## Build Mobile-First Mobile-first means you write your base CSS for the smallest screen, then layer complexity on with `min-width` queries as the viewport grows. It is not just a fashion — it produces simpler, faster CSS. Why it works better than the reverse: - **The base case is the simplest case.** A single column needs almost no layout code. You add structure as space appears, rather than tearing a complex desktop grid apart for phones. - **Overrides flow in one direction.** With `min-width`, each breakpoint only ever *adds* to what came before, which is far easier to reason about than a stack of `max-width` queries clawing things back. - **Mobile devices load less.** Phones parse the lean base styles and can skip the heavier `min-width` blocks until they apply. ```css /* Mobile-first: base is the phone layout */ .nav { display: flex; flex-direction: column; } @media (min-width: 48rem) { .nav { flex-direction: row; /* horizontal on wider screens */ justify-content: space-between; } } ``` The desktop-first equivalent would start horizontal and override to vertical, which reads backwards and tends to accumulate `!important` over time. We make the full case in [Mobile-First CSS](/mobile-first-css/), including how to migrate a legacy desktop-first stylesheet. ## Responsive Images: Don't Ship Desktop Files to Phones Images are usually the heaviest thing on a page, and sending a 2400px hero to a 375px phone wastes bandwidth and battery. Two things matter: images must scale visually, and the browser should download an appropriately sized file. The visual half is one rule you should set globally: ```css img, video { max-width: 100%; height: auto; } ``` The delivery half uses `srcset` and `sizes` so the browser picks the right file for the device's screen and pixel density: ```html A descriptive alt text ``` And `object-fit` lets an image fill a fixed box without distortion, the CSS answer to cropping: ```css .card-img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } ``` There's real nuance to `srcset` math and art direction with ``; our dedicated guide to [responsive images in CSS](/responsive-images-css/) covers it end to end. Also add `width` and `height` attributes to every `` so the browser reserves space and avoids layout shift. ## Fluid Typography and Readable Line Length Text needs to be responsive too, but the trap is scaling font size purely with `vw`, which produces microscopic text on phones and giant text on TVs. Use `clamp()` to set a floor, a fluid middle, and a ceiling: ```css :root { /* min 1rem, scales with viewport, capped at 1.25rem */ font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem); } h1 { font-size: clamp(2rem, 1.5rem + 3vw, 4rem); line-height: 1.1; } /* Keep body copy at a comfortable reading width */ .prose { max-width: 65ch; } ``` Capping paragraph width at `60–75ch` keeps lines short enough to read comfortably regardless of screen size. It is one of the cheapest quality wins in responsive design. ## Container Queries: Responsive Components, Not Just Pages Media queries ask "how wide is the *viewport*?" Container queries ask "how wide is *this component's container*?" — which is what you usually actually want. A card should switch to a horizontal layout when *its* column is wide enough, whether that column is in a sidebar or a full-width hero. Container queries are supported across all current major browsers. ```css /* Declare an element as a query container */ .card-wrapper { container-type: inline-size; } /* Style the card based on the WRAPPER's width, not the screen's */ @container (min-width: 30rem) { .card { display: grid; grid-template-columns: 12rem 1fr; } } ``` The payoff is genuinely reusable components: drop the same card into a narrow sidebar or a wide main column and it adapts to the space it's given, with zero knowledge of the page layout. This is the biggest shift in responsive design of the last few years, and it pairs naturally with the `auto-fit` grid pattern above. ## Test Like Your Users Browse A responsive layout that was only ever checked in a desktop browser window is not tested. A quick, honest checklist: - **Drag-resize the browser** slowly from wide to narrow and watch for overflow, awkward line lengths, and elements that collide. - **Use device emulation** in your browser's DevTools (responsive mode), then confirm on at least one real phone — emulators miss touch behavior and real font rendering. - **Zoom the page to 200%.** WCAG requires content to stay usable, and this catches `px`-locked layouts fast. - **Bump the browser's default font size** to something like 24px and reload. `rem`-based sizing should absorb it gracefully. - **Turn on reduced-motion and dark mode** in your OS and verify your `prefers-*` queries respond. - **Rotate a tablet** between portrait and landscape. If it survives all six, it will survive most of the real world. ## A Minimal Responsive Starter Putting the pillars together, here's a compact foundation you can drop into any project: ```css *, *::before, *::after { box-sizing: border-box; } body { margin: 0; font-size: clamp(1rem, 0.9rem + 0.4vw, 1.25rem); line-height: 1.5; } img, video { max-width: 100%; height: auto; } .container { width: min(100% - 2rem, 65rem); margin-inline: auto; } .grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); } ``` That's a fully fluid, mobile-first base with almost no media queries — because modern CSS lets the layout adapt on its own, and you add breakpoints only where content genuinely demands them. ## The Short Version Responsive design in 2026 is less about stacking media queries and more about letting CSS be intrinsically flexible. Set the viewport meta tag, size things in relative units, lean on `clamp()`, `min()`, and `auto-fit` grids to adapt without breakpoints, add media queries only where the content actually breaks, and use container queries to make components responsive on their own terms. Serve right-sized images, cap your line length, and test at real zoom levels and font sizes. Do those things and you get one codebase that looks deliberate on every screen — which is the whole point. ## Frequently asked questions **What are the three main principles of responsive web design?** The three foundations are fluid grids, flexible images, and media queries. Fluid grids size layout in relative units (%, rem, fr) so columns reflow instead of overflowing. Flexible images use max-width: 100% so they scale down to fit. Media queries then adjust the design at breakpoints. Modern CSS adds a fourth pillar: intrinsic functions like clamp() and auto-fit grids that adapt with no breakpoints at all. **How do I make a website responsive?** Start with the viewport meta tag, then build mobile-first: write base styles for the smallest screen and add complexity with min-width media queries as the viewport grows. Size spacing and type in rem, layout in fr or %, and set img { max-width: 100%; height: auto; }. Use clamp() for fluid typography and auto-fit grids so content reflows automatically. Add breakpoints only where the layout actually breaks. **What is the difference between responsive and adaptive design?** Responsive design uses one fluid layout that flexes continuously to fit any screen width. Adaptive design serves one of several fixed layouts chosen from a set of predefined breakpoints, so it snaps between discrete sizes rather than flowing smoothly. Responsive is the modern default because it handles unpredictable screen sizes, like foldables and split-screen windows, without needing a layout designed for each one. **What breakpoints should I use for responsive design?** Let content decide rather than chasing device names. A practical starting set is 30rem (~480px), 48rem (~768px), 64rem (~1024px), and 80rem (~1280px), added on top of a no-query mobile base. Resize the browser slowly and add a breakpoint wherever the layout looks cramped or lines get too long. Use rem, not px, so breakpoints scale with the user's font-size preference. **Do I still need media queries in 2026?** Fewer than before, but yes. Intrinsic functions like clamp(), min(), and auto-fit grids handle much of what once required breakpoints, and container queries let components respond to their own container instead of the viewport. Media queries are still the right tool for larger structural changes and for feature queries like prefers-reduced-motion, prefers-color-scheme, and pointer type. **Why isn't my responsive design working on mobile?** The most common cause is a missing viewport meta tag: without , phones render the page at a ~980px width and zoom out. Other frequent culprits are fixed pixel widths that don't fit small screens, images without max-width: 100%, and px-based breakpoints that ignore the user's font size. Check the meta tag first, then hunt for fixed widths. --- # Responsive Images in CSS: srcset, sizes, and object-fit > Responsive images in CSS require two jobs working together: the HTML srcset and sizes attributes tell the browser to download an appropriately sized file for each screen, while CSS properties like max-width: 100%, object-fit, and aspect-ratio control how that file displays. srcset saves bandwidth by shipping smaller files to smaller screens; CSS keeps the image fluid and prevents distortion. You need both. Source: https://thesassway.com/responsive-images-css/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Responsive images, in one sentence A responsive image ships the right file to the right screen and then fits its box without distorting. In practice that means two separate jobs: the HTML `srcset` and `sizes` attributes let the browser **download** an appropriately sized file, and CSS properties like `max-width`, `object-fit`, and `aspect-ratio` control how that file **displays** once it arrives. You need both. `srcset` saves bandwidth; CSS controls layout. Get them working together and a single `` looks crisp on a 320px phone and a 4K monitor while sending each device only the bytes it actually needs. Here is the smallest responsive image that behaves well: ```html A description of the photo ``` ```css img { max-width: 100%; height: auto; } ``` Everything below explains why each piece is there and when to reach for a different tool. ## Step one: make images stop overflowing Before anything clever, images need to respect their container. A raw `` renders at its intrinsic pixel width and will blow past a narrow layout. The fix is two lines that belong in nearly every stylesheet: ```css img { max-width: 100%; height: auto; } ``` `max-width: 100%` stops the image from ever being wider than its parent. `height: auto` lets the height scale in proportion so the picture never squashes. Use `max-width` rather than `width: 100%` so small images aren't stretched larger than their real resolution, which would make them blurry. That alone makes images *fluid*. But fluid is not the same as *responsive*: the browser is still downloading one big file and shrinking it in the viewport. A phone on a slow connection just paid for pixels it will never show. That is the problem `srcset` solves. ## srcset and sizes explained `srcset` gives the browser a menu of image files at different widths. `sizes` tells the browser how much layout space the image will occupy. The browser combines the two, factors in the device's pixel density, and picks the smallest file that still looks sharp — **before** it downloads anything. ### The width descriptor (`w`) syntax This is the common case: same image, multiple resolutions. ```html Mountain landscape at dawn ``` Reading `srcset`: each entry is a filename plus a **width descriptor** — `800w` means "this file is 800 pixels wide." You are describing the real pixel width of each file, not telling the browser which to use. Reading `sizes`: it is a list of `(media-condition) width` pairs, evaluated left to right, with a final fallback width and no condition. This example says: below 600px the image fills the viewport (`100vw`); below 1000px it takes half (`50vw`); otherwise it is a fixed `800px` wide. The browser takes the first matching rule. The browser's math is roughly: *slot width from `sizes`* × *device pixel ratio* = the ideal image width, then it grabs the nearest `srcset` file at or above that number. On a 375px phone at 2× density needing a full-width image, that's about 750px, so it downloads `hero-800.jpg` — not the 1600px file. `src` remains as a fallback for very old browsers that ignore `srcset`. `width` and `height` should match the aspect ratio of your files so the browser can reserve space and avoid layout shift. ### The pixel-density descriptor (`x`) syntax When an image is always displayed at a **fixed** CSS size — an avatar, an icon, a logo — you don't need `sizes`. Use `x` descriptors instead: ```html User avatar ``` A standard display uses `1x`; a Retina/high-DPI display grabs `2x`. Simpler, but only correct when the rendered size never changes across breakpoints. ### `w` vs `x`: which descriptor to use | Situation | Descriptor | Need `sizes`? | |---|---|---| | Image width changes with the viewport (hero, article body, cards) | `w` | Yes | | Image is always the same CSS size (avatar, icon, logo) | `x` | No | | You only have one file | neither | No | Do not mix `w` and `x` descriptors in the same `srcset` — the browser will ignore the malformed entries. ### Common `srcset` mistakes - **Omitting `sizes` with `w` descriptors.** Without it the browser assumes `100vw` and often over-downloads. Always pair `w` with `sizes`. - **Lying in `sizes`.** If you claim `400px` but CSS renders the image at `800px`, high-DPI screens look soft. Keep `sizes` roughly honest to your real layout. - **Too few steps.** Three to five widths spanning your smallest and largest render sizes is plenty; dozens of files add build cost for little gain. ## Art direction: the `` element `srcset` swaps *resolutions* of the same image. When you need a genuinely **different crop or image** at different breakpoints — a wide landscape on desktop, a tight vertical crop on mobile — reach for `` with `` and `media`: ```html Team celebrating a launch ``` The `` inside is required — it's the fallback and the element that actually renders, carrying your `alt` text. `` is also the standard way to serve modern formats with a fallback: ```html Description ``` The browser uses the first `` whose `type` it supports, falling back to the JPEG. **Use `srcset` for resolution switching and `` for art direction or format switching** — that distinction keeps your markup honest. ## object-fit: control how an image fills its box Once an image has a fixed box — say a card thumbnail that must be 300×200 regardless of the photo's shape — plain `` will distort it. `object-fit` tells the image how to fill that box without stretching. It behaves like `background-size` but for real `` elements. ```css .card__image { width: 100%; height: 200px; object-fit: cover; } ``` ### object-fit values compared | Value | What it does | Distorts? | Crops? | |---|---|---|---| | `fill` (default) | Stretches to fill the box exactly | Yes | No | | `cover` | Scales to cover the box, cropping overflow | No | Yes | | `contain` | Scales to fit entirely inside the box | No | No (may letterbox) | | `none` | Keeps intrinsic size, ignores the box | No | Yes (if larger) | | `scale-down` | Whichever of `none`/`contain` is smaller | No | No | **`object-fit: cover`** is the workhorse. It fills the box completely and crops whatever doesn't fit, so a portrait and a landscape photo both sit cleanly in the same fixed-size slot — exactly what you want for uniform card grids and avatars. `contain` is the choice when you must show the whole image (product shots, logos) and can accept empty space around it. ### object-position: choose what gets cropped With `cover`, the browser crops evenly from the center by default. `object-position` moves the visible region so a face or focal point survives the crop: ```css .card__image { width: 100%; height: 200px; object-fit: cover; object-position: top; /* keep the top; crop the bottom */ } ``` It takes the same values as `background-position` — keywords like `top`/`center`, or explicit values like `object-position: 50% 20%`. `object-fit` is supported in every modern browser and has been safe to use unprefixed for years. Older Internet Explorer never supported it, but if you're not targeting IE, ship it freely. ## Reserve space with aspect-ratio and prevent layout shift Images that pop in after the text has rendered cause **cumulative layout shift** — content jumps as each picture loads. Two defenses: **Set `width` and `height` on the ``.** Modern browsers use them only to compute the aspect ratio and reserve space, then your CSS `max-width: 100%; height: auto` takes over for actual sizing. This costs nothing and is the single easiest layout-shift fix. **Or set `aspect-ratio` in CSS** when the dimensions live in your stylesheet: ```css .card__image { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; } ``` `aspect-ratio` is supported across all current browsers. Combined with `object-fit: cover`, it locks the box shape and crops the photo to match — no reserved-space guesswork. ## Lazy loading and fetch priority Two HTML attributes round out a performance-minded image. They live on the ``, not in CSS, but they belong in any responsive-images checklist: - **`loading="lazy"`** defers off-screen images until the user scrolls near them. Perfect for images below the fold and in long lists. Never lazy-load your above-the-fold hero — it delays the largest contentful paint. - **`fetchpriority="high"`** hints the browser to fetch a critical image (usually the hero) sooner. Use it sparingly, on the one image that matters most. ```html ... ``` Both are widely supported in current browsers and degrade gracefully when they aren't. ## A production-ready pattern Putting the pieces together, here's a responsive card image that switches resolution, fills a fixed-ratio box, avoids layout shift, and lazy-loads: ```html Descriptive alt text ``` ```css .card__image { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; object-position: center; } ``` For the reasoning behind the breakpoints in that `sizes` attribute, see our guide to [CSS media queries](/css-media-queries/). This technique is one piece of a larger picture — the full [responsive web design guide](/responsive-design-guide/) covers layout, typography, and units together, and if you want to add motion or polish once the structure is solid, browse the [CSS techniques](/css-techniques/) section. ## Quick reference | Goal | Tool | Where | |---|---|---| | Stop images overflowing | `max-width: 100%; height: auto` | CSS | | Serve the right resolution | `srcset` + `sizes` (`w` descriptors) | HTML | | Serve for fixed-size images | `srcset` (`x` descriptors) | HTML | | Different crop per breakpoint | `` + `media` | HTML | | Modern formats with fallback | `` + `type` | HTML | | Fill a fixed box without distortion | `object-fit: cover` | CSS | | Aim the crop | `object-position` | CSS | | Reserve space / stop layout shift | `width`/`height` or `aspect-ratio` | HTML + CSS | | Defer off-screen images | `loading="lazy"` | HTML | Responsive images aren't one feature — they're a handful of small, well-supported tools that each do one job. Ship the right bytes with `srcset` and `sizes`, keep the layout stable with `aspect-ratio`, and control the crop with `object-fit`. Do those three things and every image on your site earns its place on every screen. ## Frequently asked questions **What is the difference between srcset and sizes?** srcset lists the image files you have and their real pixel widths (for example photo-800.jpg 800w), giving the browser a menu of resolutions. sizes tells the browser how much layout space the image will occupy at each breakpoint (for example (max-width: 700px) 100vw, 700px). The browser combines both plus the device pixel ratio to download the smallest file that still looks sharp. **What does object-fit: cover do?** object-fit: cover scales an image to completely fill its box while preserving the image's aspect ratio, cropping whatever overflows. It prevents the stretching you get from the default fill value, so portrait and landscape photos both sit cleanly in the same fixed-size slot. It is the go-to choice for uniform card grids and avatars. Pair it with object-position to control which part gets cropped. **How do I make images responsive with just CSS?** Add img { max-width: 100%; height: auto; } to keep images from overflowing their container and scaling proportionally. That makes them fluid. For true responsiveness you also want the HTML srcset and sizes attributes so smaller screens download smaller files. CSS alone shrinks one large file in the viewport; srcset actually sends fewer bytes to small devices. **When should I use the picture element instead of srcset?** Use srcset on a plain img when you are serving different resolutions of the same image. Use the picture element with source and media when you need a genuinely different crop or image at different breakpoints (art direction), or when serving modern formats like AVIF and WebP with a JPEG fallback via the type attribute. The nested img element remains required as the fallback. **Do I still need width and height on responsive images?** Yes. Setting width and height on the img lets the browser compute the aspect ratio and reserve space before the image loads, preventing cumulative layout shift where content jumps as pictures pop in. Your CSS max-width: 100%; height: auto still controls actual display size. Alternatively set aspect-ratio in CSS. Either way, reserving space is the easiest layout-shift fix available. **What descriptor should I use, w or x?** Use w descriptors (like 800w) with a sizes attribute when the image width changes across viewports, such as heroes, article images, and cards. Use x descriptors (like 2x) with no sizes when the image is always rendered at a fixed CSS size, such as avatars, icons, and logos. Never mix w and x in the same srcset, or the browser ignores the malformed entries. --- # Sass Maps Explained: @each Loops and Design Tokens in SCSS > A Sass map is a data structure that stores key-value pairs inside parentheses, like $colors: ("primary": #0d6efd, "danger": #dc3545). You read values with map.get() from the built-in sass:map module and iterate over pairs with the @each rule to generate CSS automatically. Maps are the standard way to manage design tokens in SCSS — colors, breakpoints, and spacing scales live in one variable, and loops turn them into utility classes or CSS custom properties. Source: https://thesassway.com/sass-maps-guide/ Publisher: The Sass Way Published: 2026-08-05 Updated: 2026-08-05 ## What Are Sass Maps? A Sass map is a data structure that stores key-value pairs, written as comma-separated `key: value` entries inside parentheses. Maps let you group related values — colors, breakpoints, spacing scales — under one variable, read them with `map.get()`, and loop over them with `@each` to generate CSS automatically. If you have ever maintained a dozen loose variables like `$color-primary`, `$color-secondary`, and `$color-danger`, maps are the upgrade: one source of truth instead of a pile of near-duplicates. Here is the simplest possible example: ```scss $colors: ( "primary": #0d6efd, "success": #198754, "danger": #dc3545 ); ``` That single variable now holds three named values. Everything else in this guide — lookups, loops, nesting, design tokens — builds on this one idea. Maps became widely used because they solve a real maintenance problem. When your theme colors live in a map, adding a fourth color is a one-line change, and every loop that generates classes from that map picks it up automatically. No hunting through the stylesheet for every place a color family is referenced. ## Sass Map Syntax: The Rules That Matter Map syntax is small, but a few details trip people up: - **Parentheses are required.** `$sizes: ("sm": 4px, "lg": 16px);` works; dropping the parens does not create a map. - **Keys can be any Sass value**, but strings are the most common and the easiest to reason about. Quote them consistently — Sass treats `"md"` and `md` as equal strings, but mixing styles makes code harder to scan. - **Values can be anything too**, including other maps. That is what makes nested token structures possible. - **An empty map is `()`** — the same literal as an empty list. Sass treats it as both. - **Trailing commas are allowed**, which keeps diffs clean when you add entries. One more rule worth internalizing: maps are immutable. Functions like `map.merge()` never change the original map; they return a new one. If you want to "update" a map, reassign the variable to the returned result. ## Reading Values with map.get To read a value out of a map, load the built-in `sass:map` module and call `map.get()`: ```scss @use "sass:map"; $breakpoints: ( "small": 576px, "medium": 768px, "large": 992px ); .sidebar { width: map.get($breakpoints, "medium"); } ``` Compiled CSS: ```css .sidebar { width: 768px; } ``` Two things to note. First, the `@use "sass:map";` line is required in modern Dart Sass — the old global functions like `map-get()` still compile for now, but they are deprecated in favor of the namespaced module versions, the same way `@import` is deprecated in favor of `@use`. New code should use `map.get()`. Second, if the key does not exist, `map.get()` quietly returns `null`, and Sass silently drops any declaration whose value is `null`. That silent failure is convenient sometimes and maddening at other times, which is why the next function exists. ### Guarding Lookups with map.has-key `map.has-key()` returns `true` or `false`, which lets you fail loudly instead of silently: ```scss @use "sass:map"; @function spacing($key) { @if not map.has-key($spacing, $key) { @error "Unknown spacing key: #{$key}."; } @return map.get($spacing, $key); } ``` Wrapping lookups in a small function like this is one of the highest-value habits in Sass. A typo in a key becomes a clear compile-time error pointing at the bad name, instead of a missing property you discover in the browser three days later. If you are unsure whether logic like this belongs in a function or a mixin, our comparison of [Sass mixins vs functions](/sass-mixins-vs-functions/) draws the line: functions return values, mixins output CSS. ## Looping Through a Sass Map with @each The `@each` rule is where maps earn their keep. It iterates over every pair in a map, binding the key and value to two variables: ```scss $spacing: ( "xs": 4px, "sm": 8px, "md": 16px, "lg": 32px ); @each $name, $size in $spacing { .m-#{$name} { margin: $size; } } ``` Compiled CSS: ```css .m-xs { margin: 4px; } .m-sm { margin: 8px; } .m-md { margin: 16px; } .m-lg { margin: 32px; } ``` Four classes from four lines of loop. Add `"xl": 48px` to the map and you get `.m-xl` for free on the next compile. The `#{$name}` syntax is **interpolation**, and it is mandatory here. Variables cannot appear directly inside selectors or property names — only inside values. Interpolation stamps the variable's text into the selector. Forgetting it is the single most common `@each` error, and the compiler message ("expected selector") does not always make the cause obvious. The same pattern generates any utility family you want: text colors, font sizes, z-index layers. One map, one loop, and the generated CSS never drifts out of sync with the source values. ## Nested Sass Maps Because map values can themselves be maps, you can model structured data — a full theme, for instance: ```scss @use "sass:map"; $theme: ( "color": ( "primary": #0d6efd, "danger": #dc3545 ), "font": ( "base": 16px, "heading": 32px ) ); ``` ### Reading Nested Values Modern Dart Sass lets `map.get()` take multiple keys and walk down the structure in one call: ```scss .alert { color: map.get($theme, "color", "danger"); font-size: map.get($theme, "font", "base"); } ``` Compiled CSS: ```css .alert { color: #dc3545; font-size: 16px; } ``` If you are on an older Sass version, the equivalent is chaining: `map.get(map.get($theme, "color"), "danger")`. Same result, more noise — one more reason to keep your compiler current. ### Looping Over Nested Maps To iterate a nested map, nest the loops. The outer `@each` hands you each inner map, and the inner `@each` walks its pairs: ```scss @each $category, $tokens in $theme { @each $token, $value in $tokens { // e.g. build class names like .color-primary, .font-base } } ``` A word of caution from experience: two levels of nesting is the sweet spot. Three or more levels turns your stylesheet into a data-modeling exercise, and debugging a triple-nested loop is nobody's idea of a good afternoon. If your token structure is getting that deep, flatten it with compound keys like `"color-primary"` instead. ## Design Tokens in SCSS: Maps Meet CSS Custom Properties Design tokens — named values for color, spacing, typography, and radii that a whole team shares — map perfectly onto Sass maps. The most useful modern pattern combines both worlds: define tokens in a Sass map, then loop them out as CSS custom properties. ```scss $tokens: ( "color-primary": #0d6efd, "color-surface": #f8f9fa, "radius-md": 8px, "space-md": 16px ); :root { @each $name, $value in $tokens { --#{$name}: #{$value}; } } ``` Compiled CSS: ```css :root { --color-primary: #0d6efd; --color-surface: #f8f9fa; --radius-md: 8px; --space-md: 16px; } ``` Note the `#{$value}` interpolation on the value side. Custom property values are a special case in Sass: they are treated as verbatim text, so a bare `$value` would output the literal string `$value` rather than the color. Interpolation forces evaluation. Why bother with both layers? Because they solve different problems. The Sass map gives you compile-time safety, loops, and a single editable source file. The custom properties give you runtime flexibility — theme switching, user preferences, values that JavaScript can read and change. Sass variables disappear at compile time; custom properties live on in the browser. Our guide to [CSS custom properties](/css-variables-guide/) covers that runtime side in depth, including how the two variable systems differ. ## A Practical Pattern: Breakpoint Maps and a respond() Mixin Here is the pattern that shows up in almost every serious SCSS codebase — a breakpoint map paired with a media-query mixin: ```scss @use "sass:map"; $breakpoints: ( "sm": 576px, "md": 768px, "lg": 992px, "xl": 1200px ); @mixin respond($name) { @if map.has-key($breakpoints, $name) { @media (min-width: map.get($breakpoints, $name)) { @content; } } @else { @error "Unknown breakpoint: #{$name}."; } } .card { padding: 16px; @include respond("md") { padding: 32px; } } ``` Compiled CSS: ```css .card { padding: 16px; } @media (min-width: 768px) { .card { padding: 32px; } } ``` Every breakpoint in the project now lives in exactly one place, every media query is validated at compile time, and changing your `md` breakpoint is a one-character edit. This same map-plus-mixin architecture is how frameworks like Bootstrap organize their responsive systems internally. ## Sass Map Functions Cheat Sheet All of these live in the built-in `sass:map` module (`@use "sass:map";`): | Function | What it does | |---|---| | `map.get($map, $key...)` | Returns the value for a key (or nested keys); `null` if missing | | `map.has-key($map, $key...)` | Returns `true` if the key (or nested path) exists | | `map.keys($map)` | Returns all keys as a list | | `map.values($map)` | Returns all values as a list | | `map.merge($map1, $map2)` | Returns a new map combining both; `$map2` wins on conflicts | | `map.deep-merge($map1, $map2)` | Like merge, but recurses into nested maps | | `map.set($map, $key..., $value)` | Returns a new map with one value changed | | `map.remove($map, $keys...)` | Returns a new map without the listed keys | Remember: every one of these returns a *new* map. `map.merge($defaults, $overrides)` is the standard way to build configurable components — define defaults in a map, let callers pass overrides, merge them, and read from the result. ## Common Mistakes to Avoid - **Forgetting interpolation in selectors.** `.m-$name` fails; `.m-#{$name}` works. - **Forgetting interpolation in custom property values.** `--x: $value;` outputs literal text; `--x: #{$value};` outputs the value. - **Expecting `map.get()` to error on missing keys.** It returns `null` silently — guard with `map.has-key()` or a wrapper function when a typo should fail the build. - **Treating maps as mutable.** `map.set()` does nothing unless you assign its return value. - **Over-nesting.** Two levels of structure is plenty; flatten deeper hierarchies into compound keys. ## Where to Go Next Maps and `@each` are the point where Sass stops being "CSS with variables" and becomes a small programming environment for your stylesheets. Once the pattern clicks — data in maps, CSS generated by loops — you will find uses for it everywhere: theme systems, spacing scales, utility classes, breakpoint management. If you want to see how maps fit into the bigger picture alongside partials, mixins, and the module system, [the complete guide to Sass](/complete-guide-to-sass/) walks the whole toolchain end to end. And when you build your first token system, resist the urge to model everything on day one. Start with colors and spacing in two small maps, loop them into custom properties, and grow the structure only when a real need shows up. The best token systems are boring — a short file anyone on the team can read, edit, and trust. ## Frequently asked questions **How do I get a value from a Sass map?** Load the built-in module with @use "sass:map"; then call map.get($map, $key). For example, map.get($breakpoints, "medium") returns the value stored under that key. If the key does not exist, map.get() returns null rather than throwing an error, and Sass silently drops any declaration with a null value — so guard important lookups with map.has-key() when a typo should fail the build. **How do I loop through a Sass map?** Use the @each rule with two variables: @each $key, $value in $map { ... }. Inside the loop, use interpolation — #{$key} — anywhere the key appears in a selector or property name, because bare variables are only allowed in values. This pattern is the standard way to generate utility classes: a spacing map with four entries and a three-line loop produces four margin classes automatically. **What is the difference between map.get() and the old map-get()?** They do the same lookup, but map.get() is the modern, namespaced version from the sass:map module, loaded with @use "sass:map". The old global functions like map-get() and map-merge() still compile in Dart Sass but are deprecated, in the same wave of changes that replaced @import with @use. New code should use the module functions; they also gained extras like passing multiple keys to reach into nested maps. **How do nested Sass maps work?** A map value can itself be a map, which lets you model structured data like a theme with color and font groups. Modern Dart Sass reads nested values in one call by passing multiple keys: map.get($theme, "color", "danger"). To loop a nested map, nest two @each rules — the outer loop yields each inner map, the inner loop walks its pairs. Two levels of nesting is usually the practical limit before flattening becomes easier. **Should design tokens use Sass maps or CSS custom properties?** Use both, for different jobs. Define tokens in a Sass map so you get one editable source file, compile-time validation, and loops. Then output them as CSS custom properties with @each inside :root, interpolating the value as --#{$name}: #{$value}. The custom properties handle runtime needs like theme switching and JavaScript access, while the map keeps authoring centralized. Sass variables vanish at compile time; custom properties persist in the browser. **Can I change a value in a Sass map?** Not in place — Sass maps are immutable. Functions like map.set(), map.merge(), and map.remove() always return a new map and leave the original untouched, so you must assign the result back to a variable: $config: map.merge($defaults, $overrides);. That merge pattern is the idiomatic way to build configurable components, with $overrides winning wherever both maps define the same key. --- # Sass Mixins vs Functions: When to Use Each > Use a Sass mixin when you want to inject a block of CSS declarations into a rule, invoked with @include. Use a function when you want to compute and return a single value, ended with @return. If the reusable thing outputs property: value pairs, it's a mixin; if it calculates a value like a color, length, or number, it's a function. Source: https://thesassway.com/sass-mixins-vs-functions/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Mixins vs Functions in Sass: The One-Line Answer Use a **mixin** when you want to inject a block of CSS declarations into a rule. Use a **function** when you want to compute and return a single value. The tell is simple: if the reusable thing *outputs `property: value` pairs*, it's a mixin, and you pull it in with `@include`. If it *calculates something you'll drop into a value* — a color, a length, a number — it's a function, and you call it with `@return`. That one distinction resolves almost every "which do I reach for?" question. The rest of this guide shows why, with copy-pasteable SCSS you can drop into a real project. ## The Core Difference: Output vs Value A mixin is a reusable chunk of styles. It can emit as many declarations as you like, even whole nested rules and media queries, and you place it inside a selector with `@include`. ```scss @mixin visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } .skip-link:not(:focus) { @include visually-hidden; } ``` This uses `clip-path: inset(50%)` rather than the older `clip: rect(0, 0, 0, 0)`; the legacy `clip` property is deprecated, and `clip-path` is the modern, well-supported way to collapse an element to nothing while keeping it accessible. A function takes arguments, computes something, and hands back exactly one value with `@return`. It emits no CSS on its own — you use its result *inside* a declaration. ```scss @use "sass:math"; @function rem($px, $base: 16px) { @return math.div($px, $base) * 1rem; } h1 { font-size: rem(32px); // 2rem } ``` Note the `math.div()` call — in modern Dart Sass you divide with `math.div()` after `@use "sass:math";`, not the `/` operator, which is deprecated for division. That single rule trips up a lot of older tutorials. Here's the split at a glance: | Aspect | Mixin | Function | | --- | --- | --- | | Purpose | Reuse a block of CSS declarations | Compute and return one value | | Defined with | `@mixin name` | `@function name` | | Invoked with | `@include name` | `name(args)` in a value | | Output | One or more `property: value` pairs | A single returned value | | Ends with | (emits CSS directly) | `@return` | | Can accept arguments | Yes | Yes | | Can accept a content block | Yes (`@content`) | No | | Lives inside a selector? | Yes | No — used inside a value | ## When to Reach for a Mixin Mixins shine whenever the repeated thing is *a group of declarations*, especially ones that vary by a parameter. ### Vendor-neutral patterns and property groups Anytime you'd copy-paste three or four lines together, a mixin removes the duplication: ```scss @mixin flex-center($direction: row) { display: flex; flex-direction: $direction; align-items: center; justify-content: center; } .hero { @include flex-center(column); } ``` ### Media queries with `@content` The `@content` directive lets a mixin wrap around arbitrary styles you pass in. This is the single most popular real-world use of mixins — a breakpoint helper: ```scss $breakpoints: ( "sm": 640px, "md": 768px, "lg": 1024px, ); @mixin respond($name) { $width: map.get($breakpoints, $name); @media (min-width: $width) { @content; } } .card { padding: 1rem; @include respond("md") { padding: 2rem; } } ``` That requires `@use "sass:map";` at the top of the file. Functions can't do this — they have no `@content`, and they can't emit a `@media` block. If you need to output rules, it has to be a mixin. ### Parameterised components Buttons, badges, and alerts that share structure but differ in color are a natural fit: ```scss @use "sass:color"; @mixin button-variant($bg, $fg: white) { background: $bg; color: $fg; border: 1px solid color.adjust($bg, $lightness: -8%); padding: 0.5rem 1rem; border-radius: 4px; } .btn-primary { @include button-variant(#2563eb); } .btn-danger { @include button-variant(#dc2626); } ``` Note the `color.adjust()` call. In current Dart Sass the old global color functions like `darken()` and `lighten()` are deprecated in favour of the `sass:color` module — `color.adjust($bg, $lightness: -8%)` darkens by a fixed amount, and `color.scale($bg, $lightness: -8%)` does it proportionally. Reach for the module functions so your build stays warning-free. ## When to Reach for a Function Functions exist to produce *values* you can reuse across declarations. If you catch yourself doing the same arithmetic or color math in ten places, that's a function. ### Unit math and conversions The `rem()` helper above is the classic example. Another is a fluid spacing scale: ```scss @use "sass:math"; @function space($step) { @return $step * 0.25rem; } .stack { gap: space(4); } // 1rem ``` ### Reading from a design-token map Functions pair beautifully with Sass maps to build a token lookup: ```scss @use "sass:map"; $colors: ( "brand": #2563eb, "ink": #111827, "muted": #6b7280, ); @function color($key) { @if not map.has-key($colors, $key) { @error "Unknown color: #{$key}"; } @return map.get($colors, $key); } .title { color: color("ink"); } ``` The `@error` guard is a nice touch functions enable — you get a build-time failure with a clear message instead of a silent `null`. ### Composable calculations Because a function returns a value, you can nest calls and use results in `calc()` or other functions: ```scss .sidebar { width: calc(100% - #{space(8)}); } ``` ## Arguments Work the Same in Both One thing that trips people up: mixins and functions share the *same argument system*. Everything you know about parameters applies to both, so the choice between them is never about how they take input. ### Default values Both accept default values, so callers can omit trailing arguments: ```scss @mixin elevation($level: 1) { box-shadow: 0 #{$level}px #{$level * 2}px rgba(0, 0, 0, 0.15); } .panel { @include elevation; } // uses level 1 .modal { @include elevation(4); } // overrides ``` ### Named arguments When a mixin or function has several parameters, name them at the call site to skip the ones you don't need and keep the code readable: ```scss @mixin border($width: 1px, $style: solid, $color: currentColor) { border: $width $style $color; } .divider { @include border($color: #e5e7eb); } // width and style keep defaults ``` ### Variable arguments An argument name followed by `...` collects any number of values into a list — handy for pass-through helpers: ```scss @mixin transition($props...) { transition: $props; } .link { @include transition(color 0.2s, background 0.2s); } ``` The same `...` syntax works for functions and for *spreading* a list back into a call. Because arguments behave identically, let intent — output versus value — drive your choice, never the parameters. ## `@include` vs `@return`: The Mechanics The keywords make the intent explicit, and mixing them up is the most common beginner error. - **`@include`** is how you *call a mixin*. It says "paste this mixin's declarations here." It lives inside a selector. - **`@return`** is how a *function hands back its value*. It lives inside a `@function` body, and a function must hit a `@return` on every path. A function without `@return` errors out. A mixin never uses `@return` — if you try, Sass complains that `@return` is only allowed inside a function. Think of `@include` as *"drop styles in"* and `@return` as *"give a value back."* ## Can't CSS Variables Do This Now? Partly. Native CSS custom properties handle *runtime, cascading values* — theme switches, per-component overrides — things Sass functions can't, because Sass compiles away before the browser ever runs. But custom properties can't loop, can't do build-time math across units the way `math.div()` can, and can't emit whole declaration blocks. In practice teams use both: Sass mixins and functions to generate the stylesheet, custom properties for values that must change live in the browser. Our guide to [CSS custom properties done right](/css-variables-guide/) covers where each belongs, and [what Sass actually is](/what-is-sass/) sets the broader context. ## Common Mistakes to Avoid A few errors show up again and again when developers first split logic between mixins and functions: - **Using a mixin where a function belongs.** If you only need a computed value, a mixin that sets a single property is clumsy — you can't nest it inside `calc()` or another value. Return the value from a function instead. - **Expecting a function to output rules.** Functions emit nothing on their own. Writing declarations inside a `@function` body and hoping they render is a no-op; only the `@return` value matters. - **Dividing with `/`.** In current Dart Sass, `width: $a / 2` no longer means division and will warn. Use `math.div($a, 2)` after `@use "sass:math";`. - **Forgetting the `@use` rules.** `map.get()`, `math.div()`, and friends live in built-in modules. Add `@use "sass:map";` or `@use "sass:math";` at the top of the file, or you'll get "undefined function" errors. - **Over-parameterising.** A mixin with eight arguments is usually two mixins. Keep each one focused, and lean on named arguments and defaults to keep call sites clean. ## Quick Decision Checklist Ask these in order: 1. **Does the reusable thing output `property: value` declarations?** → Mixin. 2. **Do you need to wrap a `@media` query or pass in a block of styles?** → Mixin (with `@content`). 3. **Are you computing a single value — a length, color, or number — to drop into a declaration?** → Function. 4. **Do you need a build-time guard or error on bad input?** → Function (with `@error`). 5. **Still unsure?** If you'd write it after a colon in a CSS rule, it's a function; if it fills the space *between* the braces, it's a mixin. A rule of thumb from real codebases: you'll write far more mixins than functions. Functions tend to be a small library of value helpers — `rem()`, `color()`, `space()` — while mixins carry the bulk of your reusable component and layout patterns. ## Putting It Together Mixins and functions aren't competitors; they're two halves of how Sass keeps stylesheets DRY. Functions build the vocabulary of values, and mixins assemble those values into reusable blocks. A typical file uses a function *inside* a mixin all the time: ```scss @mixin card-padding($step) { padding: space($step); // function returns the value... } // ...mixin emits the declaration ``` Once the output-vs-value distinction clicks, you'll pick the right tool without thinking about it. For the full picture — setup, partials, maps, and file structure — see our [complete guide to Sass](/complete-guide-to-sass/), or browse more [Sass and SCSS tutorials](/sass-and-scss/) to go deeper on any piece. ## Frequently asked questions **What is the difference between a mixin and a function in Sass?** A mixin outputs a block of CSS declarations and is placed inside a selector with @include. A function computes and returns a single value using @return, which you then use inside a declaration. Mixins produce styles; functions produce values. Mixins can also accept a @content block and emit media queries, which functions cannot do. **When should I use a Sass mixin?** Reach for a mixin whenever the reusable thing is a group of CSS declarations: property clusters like flex centering, parameterised component styles like button variants, or breakpoint helpers that wrap a @media query using @content. If you find yourself copy-pasting several declarations together, a mixin removes the duplication. **What is a Sass function example?** A common example is a pixel-to-rem converter: @function rem($px, $base: 16px) { @return math.div($px, $base) * 1rem; }. You call it inside a value, like font-size: rem(32px), which returns 2rem. Functions are ideal for unit math, reading values from design-token maps, and any calculation you reuse across declarations. **What is the difference between @include and @return in Sass?** @include calls a mixin and pastes its declarations into the current selector. @return sits inside a function and hands back a single computed value. A function must reach a @return on every path, and using @return inside a mixin is an error. Think of @include as dropping styles in and @return as giving a value back. **Can a Sass function output CSS declarations?** No. A function can only compute and return one value with @return; it cannot emit property: value pairs or wrap a @media block. If you need to output declarations or use a @content block, you must use a mixin. Functions are used inside a value, not as a standalone block within a selector. **Do CSS custom properties replace Sass mixins and functions?** Not fully. CSS custom properties handle runtime, cascading values like theme switches that Sass cannot, since Sass compiles away before the browser runs. But custom properties can't loop, do build-time unit math, or emit declaration blocks. Most teams use both: Sass mixins and functions to generate CSS, custom properties for values that change live. --- # Sass Partials: File Structure and CSS Ownership > Sass partials are source files, conventionally named with a leading underscore, that another stylesheet loads instead of treating as separate build entries. Organize a small project around one CSS entry file, reusable value modules, and component partials. Use modern Dart Sass modules with explicit dependencies. Keep a record of which files emit CSS, then compile and inspect each intended output in your own environment before shipping. Source: https://thesassway.com/sass-partials-file-structure/ Publisher: The Sass Way Published: 2026-09-08 Updated: 2026-09-08 ## What are Sass partials for? Sass partials are source files, conventionally named with a leading underscore, that another stylesheet loads instead of treating as separate build entries. Organize a small project around one CSS entry file, reusable value modules, and component partials. Use modern Dart Sass modules with explicit dependencies. Keep a record of which files emit CSS, then compile and inspect each intended output in your own environment before shipping. This is a source-led, five-file reference project, not a framework template. Its purpose is to make three questions answerable: where does a value live, which component owns a selector, and which file produces the stylesheet? The folder names are proposed editorial conventions, not requirements imposed by Sass. For language fundamentals beyond file organization, use the [complete Sass guide](/complete-guide-to-sass/). This page concentrates on one small dependency structure rather than a whole-language introduction. ## Decide which files own CSS before creating folders Start with an output inventory. For this example, the intended deliverable is one stylesheet containing a card rule and a notice rule. There is no separate token stylesheet and no standalone component stylesheet. Use the following ownership table when creating the files: | File | Responsibility | Intended CSS contribution | |---|---|---| | `app.scss` | Select the components in the bundle | Loads their rules | | `tokens/_spacing.scss` | Define two shared values | None | | `tokens/_index.scss` | Expose the token interface | None in this example | | `components/_card.scss` | Own the card selector | One `.card` rule | | `components/_notice.scss` | Own the notice selector | One `.notice` rule | Sass distinguishes statements that generate CSS from declarations used during compilation. A file containing only the variables below has no selector to contribute. A file containing a style rule does. The underscore alone does not decide that distinction. [Sass stylesheet structure](https://sass-lang.com/documentation/syntax/structure/) This table is also a useful review boundary. If someone later adds a global reset to the token directory, the change alters the directory's output responsibility. Review that as an architectural change, not merely another line of configuration. ## Create one small, complete source tree Create these five files in a scratch project. The paths are relative to that project's root: ```text styles/ app.scss tokens/ _spacing.scss _index.scss components/ _card.scss _notice.scss ``` Use lowercase filenames and consistent spelling. Write module URLs with forward slashes, including on Windows. Sass resolves relative module URLs from the file containing the load, and its URL casing matters across platforms. [Sass module resolution](https://sass-lang.com/documentation/at-rules/use/#finding-the-module) First, put the shared values in `styles/tokens/_spacing.scss`: ```scss $gap: 1rem; $radius: 0.5rem; ``` These are illustrative design values, not a prescribed spacing scale. Sass variables are evaluated during compilation; their names are not browser custom properties. [Sass variables](https://sass-lang.com/documentation/variables/) Next, give the directory a deliberate interface in `styles/tokens/_index.scss`: ```scss @forward "spacing"; ``` The index exposes the spacing module's public members to consumers. Forwarding does not itself make those members locally available in the forwarding file; that would require a separate `@use`. Nor is forwarding inherently CSS-free: a forwarded module's styles are included too. This index emits none because the spacing file contains only values. [Sass @forward](https://sass-lang.com/documentation/at-rules/forward/) Add the complete card component in `styles/components/_card.scss`: ```scss @use "../tokens"; .card { padding: tokens.$gap; border-radius: tokens.$radius; } ``` Then add `styles/components/_notice.scss`: ```scss @use "../tokens"; .notice { margin-block: tokens.$gap; padding-inline: tokens.$gap * 2; } ``` Finally, select those components in `styles/app.scss`: ```scss @use "components/card"; @use "components/notice"; ``` The folder URL loads its index, and each component declares its own dependency. A load in `app.scss` would not make members automatically available inside the component files. Keep `@use` declarations before style rules. [Sass module loading](https://sass-lang.com/documentation/at-rules/use/) All five files are needed for this reference project. Do not paste the five blocks sequentially into one file: the exercise is specifically about file boundaries and relative dependencies. ## Trace the dependency direction Read this project from its output entry toward its values: ```text app.scss -> components/_card.scss -> tokens/_index.scss -> tokens/_spacing.scss -> components/_notice.scss -> tokens/_index.scss -> tokens/_spacing.scss ``` The proposed direction is entry → components → tokens. No token file depends on a component. Neither component depends on the application entry. That makes it possible to inspect a component's inputs without navigating the entire application. Suppose a second component needs the same radius. It can consume the token interface. Suppose it needs the card's selector rules instead. That is a different dependency: decide whether both components should be selected by the entry, rather than making one component load an unrelated component simply to obtain a value. This is a review heuristic, not a compiler restriction on folder names. A project can choose other boundaries. The important property is that the team can explain each arrow and identify the output it brings. ## Compile the entry and inspect the result Use an already-installed Dart Sass CLI, invoked according to your project's setup. From the scratch project's root, this documented command prints expanded CSS to the terminal rather than naming an output file: ```bash sass --style=expanded --no-source-map styles/app.scss ``` Here, `sass` means the executable provided by your environment; a project-local installation may need invocation through its existing package script. Check that environment's compiler version before comparing results. The official CLI documents single-file compilation, terminal output when the output path is omitted, and expanded formatting. It also documents directory compilation skipping underscore-prefixed partials. That is why `app.scss` is the intended entry in this tree, rather than four extra component or token outputs. [Dart Sass CLI](https://sass-lang.com/documentation/cli/dart-sass/) The expected expanded result for the reference files is: ```css .card { padding: 1rem; border-radius: 0.5rem; } .notice { margin-block: 1rem; padding-inline: 2rem; } ``` This reference output has not yet been verified with a local compiler. Treat it as a comparison target pending compilation, not as a tested result or production-ready artifact. The arithmetic is explicit: the notice's horizontal padding is `1rem × 2 = 2rem`. The card radius remains `0.5rem`. Look for those declarations, the two intended selectors, and the absence of an unexpected global selector from the token files. Compilation is only one check. It does not establish that these classes are used correctly by HTML, that the visual design meets accessibility needs, or that the actual build tool packages the stylesheet as intended. Test those concerns separately in the application. ## Use an output review sheet when splitting an existing file For an existing stylesheet, preserve a baseline build before moving declarations. Make one structural change at a time and compare the generated result. An example review sheet for this reference project is: | Review question | Expected answer for this project | |---|---| | Which entry is compiled? | `styles/app.scss` | | Which selectors are intended? | `.card` and `.notice` | | Which file owns the card radius? | Value in spacing; declaration in card | | Which files should add no selectors? | Both token files | | Is a changed appearance intentional? | No visual change intended from file movement alone | | What must still be tested? | Actual compiler output and application rendering | Do not combine file extraction with a redesigned spacing scale, renamed classes, and a compiler upgrade in one unexamined change. If the output differs, those simultaneous changes make the cause harder to isolate. For a useful first extraction, select a component whose selectors already form a coherent group. Move its rules, declare its value dependencies, and update the entry. Then inspect the diff. Repeat only after the earlier change is understood. The [Tooling & Workflow hub](/tooling-and-workflow/) groups related build topics. The [Sass & SCSS hub](/sass-and-scss/) covers language features that may justify adding another module. ## Keep index files and build entries distinct An index is an interface for consumers; a CSS entry is a build target. In this project, the token index is a partial while app is the build target. Do not create a nonpartial index merely because every directory feels incomplete without one. Likewise, avoid exposing the whole component directory through the token index. That would turn a consumer's request for spacing values into a request for component styles. Keep the ownership table accurate when choosing what to forward. A second application entry deserves a second output inventory. Sass's module system is not a website-wide optimizer that removes every repeated selector from independently generated bundles. Check what each build entry produces and what the page actually loads before making a duplication or download-size claim. For new module organization, use `@use` and `@forward`. Sass `@import` was deprecated in Dart Sass 1.80.0; a file-structure guide should not introduce it as the current default. [Sass import deprecation](https://sass-lang.com/documentation/breaking-changes/import/) ## Frequently asked questions ### Does every Sass file need an underscore? No. In this proposed structure, app.scss is the intended CSS build entry, while the four files it depends on are partials. The distinction records the build plan; it is not a rule that every stylesheet must use the same naming pattern. Check the actual compilation command as well as the filenames when deciding which outputs belong in the project. ### Does a partial prevent its styles from appearing in CSS? No. The card partial in this example owns a .card rule that belongs in app's generated CSS. Its underscore prevents an additional standalone card output during Dart Sass directory compilation; it does not suppress that rule when app loads the component. Keep value-only files separate from files containing selectors if consumers need reusable values without component styles. ### Why does each component load the tokens folder? Both components in the example read values from that folder's public entrypoint. Declaring that dependency in each component makes its inputs visible without first inspecting app.scss. For this proposed architecture, app selects components and components select values; a component should not need to load app merely to obtain a spacing value. Review the direction before adding another dependency. ### Should every folder contain an index file? No. Here, the tokens index is a deliberate public boundary between its consumers and the spacing file. The components folder has no index because app names the two components explicitly. Add an index when it gives consumers a useful stable interface, and record what it exposes and emits. An index that merely hides an unexplained dependency list does not improve ownership. ### Will reorganizing Sass files reduce the CSS download? Not necessarily. Moving the same selectors into smaller source files can leave the generated CSS unchanged. In this example, success means a clearer dependency structure and the intended two rules, not a claimed performance improvement. Compare output size and content using the project's actual build, and test the rendered result before describing a reorganization as an optimization. ## Sources - [Sass: stylesheet structure](https://sass-lang.com/documentation/syntax/structure/) - [Sass: variables](https://sass-lang.com/documentation/variables/) - [Sass: @use and module resolution](https://sass-lang.com/documentation/at-rules/use/) - [Sass: @forward](https://sass-lang.com/documentation/at-rules/forward/) - [Dart Sass: command-line interface](https://sass-lang.com/documentation/cli/dart-sass/) - [Sass: import deprecation](https://sass-lang.com/documentation/breaking-changes/import/) ## Frequently asked questions **Does every Sass file need an underscore?** No. In this proposed structure, app.scss is the intended CSS build entry, while the four files it depends on are partials. The distinction records the build plan; it is not a rule that every stylesheet must use the same naming pattern. Check the actual compilation command as well as the filenames when deciding which outputs belong in the project. **Does a partial prevent its styles from appearing in CSS?** No. The card partial in this example owns a .card rule that belongs in app's generated CSS. Its underscore prevents an additional standalone card output during Dart Sass directory compilation; it does not suppress that rule when app loads the component. Keep value-only files separate from files containing selectors if consumers need reusable values without component styles. **Why does each component load the tokens folder?** Both components in the example read values from that folder's public entrypoint. Declaring that dependency in each component makes its inputs visible without first inspecting app.scss. For this proposed architecture, app selects components and components select values; a component should not need to load app merely to obtain a spacing value. Review the direction before adding another dependency. **Should every folder contain an index file?** No. Here, the tokens index is a deliberate public boundary between its consumers and the spacing file. The components folder has no index because app names the two components explicitly. Add an index when it gives consumers a useful stable interface, and record what it exposes and emits. An index that merely hides an unexplained dependency list does not improve ownership. **Will reorganizing Sass files reduce the CSS download?** Not necessarily. Moving the same selectors into smaller source files can leave the generated CSS unchanged. In this example, success means a clearer dependency structure and the intended two rules, not a claimed performance improvement. Compare output size and content using the project's actual build, and test the rendered result before describing a reorganization as an optimization. --- # Sass @use vs @import: How to Migrate Off Deprecated Imports > @use is the modern replacement for Sass's deprecated @import rule. @use loads each file once as a module and namespaces its variables, mixins, and functions, while @import loads files repeatedly into one global scope, causing name collisions and duplicated CSS. Dart Sass has warned on @import since version 1.80.0 (October 2024), with removal planned for Dart Sass 3.0. Migrate by running the official sass-migrator tool, then adopt @forward for library entrypoints. Source: https://thesassway.com/sass-use-vs-import/ Publisher: The Sass Way Published: 2026-08-04 Updated: 2026-08-04 ## @use vs @import: The Short Answer `@use` is the modern way to load one Sass file from another, and `@import` is the deprecated way. The practical difference: `@use` loads each file exactly once and gives its variables, mixins, and functions a namespace, while `@import` dumps everything into one global scope and re-compiles a file every time it's imported. Dart Sass has emitted deprecation warnings for `@import` since version 1.80.0 (released in October 2024), and the rule is scheduled for removal in Dart Sass 3.0. If you're starting a new project today, use `@use` and `@forward` exclusively. If you're maintaining an older codebase full of `@import` statements, the migration is more mechanical than scary — and Sass ships an official tool that does most of it for you. This guide covers both. If you're newer to Sass itself, start with [what Sass is and why it exists](/what-is-sass/) or our [complete guide to Sass](/complete-guide-to-sass/); this article assumes you already write SCSS and want to modernize how your files talk to each other. ## Why Sass Deprecated @import `@import` looks harmless, and for small projects it mostly is. At scale, it causes four recurring problems that the Sass team spent years watching developers fight. ### Everything lands in one global scope When you write `@import 'buttons';`, every variable, mixin, and function in `_buttons.scss` becomes globally available — along with everything *that* file imported, and so on down the chain. In a large codebase you end up with hundreds of globals and no way to tell where `$accent-color` was actually defined. Two partials that both define `$spacing` silently overwrite each other based on import order. ### Files get compiled more than once `@import` is textual inclusion, like copy-pasting the file's contents in place. If `_buttons.scss` and `_forms.scss` both import `_mixins.scss`, and your main file imports both, any CSS rules inside `_mixins.scss` are emitted twice in your compiled output. Teams worked around this with mixin-only partials and careful conventions, but the language itself never protected you. ### No privacy With `@import`, a library can't have internal helpers. Every `-private-function` you write is technically callable by anyone who imports your file. The convention of prefixing internals with a dash or underscore was just that — a convention. ### It collides with CSS's own @import Plain CSS has its own `@import` rule for loading stylesheets over the network. Sass had to guess which one you meant based on file extensions and URL patterns. `@use` removes the ambiguity entirely: it's unambiguously a Sass feature, and `@import` can eventually go back to meaning exactly what it means in CSS. ## How @use Works `@use` loads another Sass file as a *module*. The file is compiled once, its CSS is included once, and its members are exposed under a namespace. ```scss // _corners.scss $radius: 3px; @mixin rounded { border-radius: $radius; } ``` ```scss // buttons.scss @use 'corners'; .button { @include corners.rounded; padding: 5px + corners.$radius; } ``` Compiled output: ```css .button { border-radius: 3px; padding: 8px; } ``` Two things to notice. First, you reference members through the namespace: `corners.rounded`, `corners.$radius`. Second, the namespace defaults to the last component of the URL, minus any leading underscore and file extension — `@use 'src/corners'` still gives you `corners`. One structural rule: `@use` rules have to come before any style rules in the file. They aren't required to be the very first lines, though — `@charset`, `@forward`, and variable declarations may precede them, which is how you define a value locally and then pass it into a module's configuration. What you can't do is conditionally `@use` something halfway down a stylesheet the way people sometimes did with `@import`. ### Choosing a namespace (or none) You can rename the namespace with `as`, which is handy for long file names: ```scss @use 'typography-helpers' as type; h1 { @include type.heading(2rem); } ``` And you can drop the namespace entirely with `as *`: ```scss @use 'corners' as *; .card { @include rounded; } ``` Use `as *` sparingly — it gives up the collision safety you migrated for. It's reasonable for a single core config file that everything depends on; it's a bad habit as a default. ### Configuring modules with `with` `@use ... with` replaces the old pattern of redefining variables before an `@import`. It only works on variables the module marks as configurable with `!default`: ```scss // _theme.scss $primary: #036 !default; $radius: 4px !default; .badge { background: $primary; border-radius: $radius; } ``` ```scss // site.scss @use 'theme' with ( $primary: #d81b60 ); ``` The compiled `.badge` rule uses `#d81b60` for its background and keeps the default `4px` radius. A module can only be configured the *first* time it's loaded, which is another reason to centralize configuration in one entrypoint file. ### Private members Prefix a member with `-` or `_` and it becomes genuinely private — invisible outside its own file: ```scss // _helpers.scss $-base-unit: 8px; // not accessible from other files @function spacing($multiplier) { @return $-base-unit * $multiplier; } ``` ### Built-in modules The module system also reorganized Sass's built-in functions. Instead of a flat global list, they live in modules like `sass:math`, `sass:color`, `sass:string`, and `sass:list`: ```scss @use 'sass:math'; @use 'sass:color'; .col { width: math.div(4, 12) * 100%; border-color: color.adjust(#036, $lightness: 20%); } ``` This matters for migration because the old global functions are being phased out too. `darken()` and `lighten()` are discouraged in favor of `color.adjust()` or `color.scale()`, and slash division (`$width / 3`) has been replaced by `math.div()`. Recent Dart Sass versions emit deprecation warnings for the global versions of most built-in functions, so a full modernization pass usually touches these at the same time. ## @forward: The Other Half of the Module System `@use` loads a module for *your own* use. `@forward` re-exports a module's members so that files loading *your* file can see them. You need it any time you split a library across multiple partials but want consumers to load a single entrypoint. Say your library lives in a folder called `library/`: ```scss // library/_variables.scss $primary: #036 !default; ``` ```scss // library/_mixins.scss @mixin visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } ``` ```scss // library/_index.scss @forward 'variables'; @forward 'mixins'; ``` The URLs in `_index.scss` are resolved relative to `_index.scss` itself, so `@forward 'variables'` picks up `library/_variables.scss` — no folder prefix needed. Now a consumer loads the whole library through one path: ```scss // site.scss @use 'library'; .cta { background: library.$primary; } ``` Compiled output: ```css .cta { background: #036; } ``` Note the folder name, not a file name: when you `@use` a directory, Sass automatically loads the `_index.scss` (or `_index.sass`) inside it, and the namespace comes from the directory — `library`. That combination — a folder of focused partials plus an `_index.scss` full of `@forward` rules — is the standard way to structure a modern Sass library or design system. `@forward` also supports visibility control (`show` and `hide`) and prefixing (`as btn-*`), which lets a library expose a curated public API instead of everything it happens to contain. Importantly, `@forward` does *not* make members available in the forwarding file itself; if `_index.scss` also needs `$primary`, it must `@use` the file too. ## @use vs @import at a Glance | Behavior | `@import` | `@use` | |---|---|---| | Status | Deprecated (warnings since Dart Sass 1.80.0) | Current standard | | Scope | Everything global | Namespaced per module | | File loaded | Every time it's imported | Exactly once per compilation | | Duplicate CSS output | Possible | Prevented | | Private members | Convention only | Enforced with `-`/`_` prefix | | Configuration | Redefine variables before import | Explicit `with (...)` syntax | | Placement | Anywhere in the file | Before any style rules | | Re-exporting | Automatic (everything leaks through) | Explicit via `@forward` | ## How to Migrate from @import to @use ### Step 1: Confirm you're on Dart Sass `@use` is only implemented in Dart Sass — LibSass (the engine behind the old `node-sass` package) was deprecated in 2020 and never supported the module system. If your build still uses `node-sass`, switch to the `sass` package first. Our guide to [installing and compiling Sass](/how-to-install-sass/) walks through the setup. ### Step 2: Run the official migrator The Sass team maintains a migration tool that rewrites `@import` rules, adds namespaces to every reference, and converts global built-in functions to their module equivalents: ```bash npm install -g sass-migrator sass-migrator module --migrate-deps your-entrypoint.scss ``` The `--migrate-deps` flag tells it to follow and migrate every file your entrypoint loads, so one command typically converts an entire project. Commit your work first, run it, and review the diff. The `module` migrator handles the module system itself. Slash division is a separate migration with its own command, so if you still have `$a / $b` anywhere, run that pass too: ```bash sass-migrator division --migrate-deps your-entrypoint.scss ``` That one rewrites division expressions to `math.div()` and adds the `@use 'sass:math'` rule where it's needed. Running `module` alone will not silence slash-division deprecation warnings. ### Step 3: Clean up what the tool can't decide The migrator is good, but a few judgment calls remain yours: - **Collapse shared globals into a config module.** If dozens of files used the same imported variables, consider a single `_config.scss` that partials `@use` explicitly, so dependencies are visible. - **Add `@forward` entrypoints.** Group related partials into folders with an `_index.scss`, so consumers load one path instead of six. - **Audit any `as *` output.** The migrator sometimes preserves global-style access where namespacing would break things; tighten these up over time. - **Mark intended-private members** with a leading `-` so the compiler enforces what used to be convention. ### Step 4: Handle third-party libraries Libraries written for `@import` can still be loaded with `@use` — namespacing works on any Sass file. Configuration is the sticking point: older libraries that expect you to define variables *before* importing them need the `with (...)` syntax instead, which works as long as the library declared its variables with `!default`. Bootstrap (which has been Sass-based since v4) and most maintained libraries document a module-system approach; check for an updated integration guide before hand-rolling one. ## Common Gotchas **"Undefined variable" after migrating.** Almost always a missing namespace — `$primary` needs to become `theme.$primary`, or the file needs its own `@use` statement. Under `@use`, every file explicitly loads what it depends on; nothing arrives ambiently anymore. The quickest diagnosis is to search the project for the variable's declaration and add a `@use` for that partial at the top of the failing file. Watch for privacy too: if a helper picked up a leading `-` or `_` during cleanup, it's now invisible outside its own file, so cross-partial callers need it forwarded deliberately or renamed back. **Namespace collisions between same-named partials.** `@use 'buttons'` and `@use 'admin/buttons'` both default to the namespace `buttons`, and Sass refuses to compile a file that loads both. Rename one at the call site with `@use 'admin/buttons' as admin-buttons`, or route both through a single `@forward` entrypoint that applies an `as` prefix to one of them. **A module was "already loaded" and can't be configured.** `with` only works the first time a module is loaded in a compilation, and "first" means first in load order — not first in the file you consider most important. If a partial buried deep in the tree does a plain `@use 'theme'` before your entrypoint reaches `@use 'theme' with (...)`, the compile fails. The durable fix is to funnel configuration through one file: a `_configured-theme.scss` that does the configured `@use` and then `@forward`s the module, with every other file loading that instead. **Duplicate CSS disappeared — and that changed the cascade.** If your old build emitted a partial's rules twice, the later copy was winning on source order; the two copies had identical specificity, so whichever came last applied. Deduplication is exactly what you migrated for, but it can expose a rule that was quietly being overridden by its own twin. Diff your compiled CSS before and after, and look closely anywhere a selector's declarations moved earlier in the output. **Indented-syntax projects work identically.** Everything here applies to both SCSS and the indented syntax — see [Sass vs SCSS](/sass-vs-scss/) if you're weighing the two. ## The Bottom Line `@use` and `@forward` fix real problems: global-namespace collisions, duplicated output, and invisible dependencies. `@import` still compiles today, but it prints warnings now and has a removal date on the calendar, so every month of delay makes the eventual migration slightly larger. Run `sass-migrator` on a branch this week — for most projects the whole exercise, including review, fits in an afternoon, and your stylesheets come out easier to reason about than they went in. ## Frequently asked questions **Is @import deprecated in Sass?** Yes. Dart Sass has emitted deprecation warnings for @import since version 1.80.0, released in October 2024, and the Sass team plans to remove the rule in Dart Sass 3.0. Your existing stylesheets still compile today, but every @import prints a warning. New code should use @use and @forward instead, and existing projects should plan a migration rather than waiting for the removal release. **What is the difference between @use and @import in Sass?** @use loads a file once as a module and exposes its variables, mixins, and functions under a namespace, like buttons.$radius. @import is textual inclusion: it dumps everything into the global scope and re-includes a file every time it is imported, which can duplicate CSS output and cause silent name collisions. @use also adds real privacy via dash-prefixed members and explicit configuration with the with (...) syntax. **How do I migrate from @import to @use automatically?** Use the official migrator maintained by the Sass team. Install it with npm install -g sass-migrator, then run sass-migrator module --migrate-deps your-entrypoint.scss. The --migrate-deps flag follows every file your entrypoint loads, rewrites @import rules to @use, adds namespaces to references, and renames global built-in functions to their module equivalents, such as adjust-color() becoming color.adjust(). Slash division is handled by a separate command: run sass-migrator division --migrate-deps on the same entrypoint to convert $a / $b into math.div($a, $b). Commit first and review each diff afterward. **What does @forward do in Sass?** @forward re-exports another file's members so that stylesheets loading your file can use them. It is how you build a single entrypoint for a library split across many partials: a library/_index.scss containing @forward rules for each partial in that folder, which consumers then load with @use 'library'. It supports show and hide for visibility control and as prefix-* for renaming. Unlike @use, @forward does not make members available inside the forwarding file itself. **Can I still use @use with old Sass libraries built for @import?** Usually, yes. Namespacing works on any Sass file, so @use 'library' generally loads older libraries fine. The catch is configuration: instead of redefining variables before an @import, you pass overrides with @use 'library' with ($primary: red), which works as long as the library declared its variables with !default. Check the library's documentation first, since most maintained projects now publish module-system instructions. **Why am I getting undefined variable errors after switching to @use?** Because @use removes ambient globals. Every file must explicitly load what it depends on, and references need namespaces: $primary becomes theme.$primary after @use 'theme'. If a variable is genuinely missing, add a @use statement for the file that defines it at the top of the stylesheet. Also check for dash- or underscore-prefixed members, which are private under the module system and inaccessible from other files. --- # Sass Variables: How to Declare, Scope, and Reuse Them > Sass variables store reusable values — colors, sizes, font stacks — declared with a dollar sign, like $brand-color: #1f7a5e;. You reference them anywhere a CSS value goes, and at compile time Sass replaces each reference with its value, so the browser only sees plain CSS. Variables declared at the top level are global; those declared inside a block are local to it. The !default flag makes a variable overridable, which powers configurable libraries via @use ... with. Source: https://thesassway.com/sass-variables-guide/ Publisher: The Sass Way Published: 2026-08-21 Updated: 2026-08-21 ## What Are Sass Variables? A Sass variable is a named value — a color, a size, a font stack, almost anything — that you declare once with a `$` prefix and reuse throughout your stylesheets. You declare one with `$name: value;` and reference it anywhere a value could go. When Sass compiles to CSS, every variable reference is replaced with its value, so the browser only ever sees plain CSS. ```scss $brand-color: #1f7a5e; $base-spacing: 1rem; .button { background: $brand-color; padding: $base-spacing $base-spacing * 2; } ``` Compiles to: ```css .button { background: #1f7a5e; padding: 1rem 2rem; } ``` That's the whole core idea: define a value in one place, use it in fifty, change it in one. If your brand color shifts next quarter, you edit a single line instead of hunting through every file. Variables were the feature that first pulled most developers into Sass, and they're still the fastest win you get from it. If you're brand new to the language itself, start with [what Sass is and why it exists](/what-is-sass/), then come back here. ## How to Declare a Sass Variable The syntax is deliberately close to a CSS declaration: ```scss $variable-name: value; ``` A few rules worth knowing up front: - **Variables start with `$`.** `$primary`, `$gutter-width`, `$font-stack` — the dollar sign is what tells Sass this is a variable, not a property. - **Hyphens and underscores are interchangeable.** `$font-size` and `$font_size` refer to the *same* variable. This is a historical quirk; pick hyphens and stay consistent. - **Declarations need a value.** Unlike some languages, you can't declare an empty variable — but you can assign `null`, and a property whose value resolves to `null` is simply omitted from the compiled CSS. ### What can a variable hold? Sass variables aren't limited to colors. They can store any Sass data type: - **Numbers**, with or without units: `$columns: 12;`, `$gutter: 24px;` - **Strings**: `$font-stack: "Inter", sans-serif;` - **Colors**: `$danger: #c0392b;` - **Booleans**: `$enable-shadows: true;` - **Null**: `$border: null;` - **Lists**: `$sizes: 8px 16px 24px;` - **Maps** (key–value pairs): `$breakpoints: (small: 480px, medium: 768px, large: 1024px);` Lists and maps are where variables graduate from "handy" to "architectural" — a single `$breakpoints` map can drive every media query on your site. ## Sass Variable Scope: Global vs Local Scope determines where a variable can be used. Sass has two levels: - **Global variables** are declared at the top level of a file, outside any selector or block. They're available everywhere after the point of declaration. - **Local variables** are declared inside a block — a selector, mixin, function, or other `{ }` — and only exist within that block and its children. ```scss $accent: #1f7a5e; // global .card { $accent: #c0392b; // local — shadows the global inside .card border-color: $accent; } .footer { border-color: $accent; // still the global value } ``` Compiles to: ```css .card { border-color: #c0392b; } .footer { border-color: #1f7a5e; } ``` Notice what happened in `.card`: declaring a local variable with the same name as a global one doesn't overwrite the global. It **shadows** it — the local value wins inside that block, and the global value remains untouched everywhere else. This is a feature, not a bug: mixins and functions can use short internal variable names without worrying about trampling your globals. ### Overriding a global from inside a block: `!global` If you genuinely want an assignment inside a block to change the global variable, add the `!global` flag: ```scss $theme: light; @mixin activate-dark { $theme: dark !global; } ``` Use this sparingly. Modern Dart Sass expects the global variable to already exist at the top level — using `!global` to invent a brand-new variable from inside a block is deprecated. In practice, code that needs `!global` often signals a structure that would be cleaner with maps or module configuration. ### One quirk: flow control scope Variables assigned inside `@if`, `@each`, `@for`, or `@while` behave differently: they *can* update a variable from the surrounding scope without `!global`, but a *new* variable first declared inside the flow-control block isn't visible outside it. So declare the variable before the loop, then modify it inside: ```scss $total: 0; @each $size in (8px 16px 24px) { $total: $total + $size; } // $total is now 48px ``` ## The `!default` Flag: Variables Made for Overriding `!default` assigns a value to a variable **only if that variable is undefined or null**. If it already has a value, the assignment is skipped: ```scss $accent: #7c4dff !default; ``` This one flag is the backbone of every configurable Sass library. Frameworks like Bootstrap declare hundreds of variables with `!default` so that *your* values, if you provide them, win over the library's fallbacks. ### Configuring a module with `@use ... with` In the modern module system, you override `!default` variables at import time: ```scss // _theme.scss $accent: #7c4dff !default; $radius: 4px !default; // main.scss @use 'theme' with ( $accent: #ff5722 ); ``` Inside `main.scss`, `theme.$accent` is `#ff5722` and `theme.$radius` keeps its default of `4px`. Only variables marked `!default` can be configured this way — attempting to configure one without the flag is a compile error, which is exactly the guardrail you want in a shared codebase. ## Sharing Variables Across Files with `@use` Real projects keep variables in a dedicated partial and load it where needed. The modern way is `@use`, which namespaces everything it loads: ```scss // _tokens.scss $brand: #1f7a5e; $radius: 6px; // button.scss @use 'tokens'; .button { background: tokens.$brand; border-radius: tokens.$radius; } ``` The namespace (`tokens.`) tells you exactly where every value comes from — a genuine readability win in large codebases. If the prefix feels heavy, you can rename it (`@use 'tokens' as t;`) or drop it entirely (`@use 'tokens' as *;`), though the wildcard form gives up the main benefit. Avoid the older `@import` for new code: it dumps every variable into one global soup, and the Sass team has deprecated it in favor of `@use` and `@forward`. Our [complete guide to Sass](/complete-guide-to-sass/) covers structuring partials and modules in depth. ## Sass Variables vs CSS Variables (Custom Properties) CSS now has native variables, so which should you use? They solve different problems, and the honest answer for most projects is *both*. | | Sass variables | CSS custom properties | |---|---|---| | Syntax | `$name: value;` | `--name: value;` used via `var(--name)` | | Resolved | At compile time | At runtime, in the browser | | Exists in final CSS? | No — replaced by values | Yes — stays live in the cascade | | Can JavaScript change it? | No | Yes | | Responds to cascade/inheritance? | No | Yes — can differ per element or theme | | Usable in media query conditions? | Yes | No | | Can hold non-value logic (maps, lists for loops)? | Yes | No | The rule of thumb: - **Sass variables** for values fixed at build time: breakpoints, grid math, values you loop over, anything feeding Sass functions or mixins. - **CSS custom properties** for values that change at runtime: theme switching, user preferences, per-component overrides. One gotcha when combining them: inside a custom property declaration, Sass treats the value as opaque, so you must interpolate: ```scss $brand: #1f7a5e; :root { --brand: #{$brand}; // interpolation required } ``` Without `#{ }`, the compiled CSS would contain the literal text `$brand`. For the runtime side of this story, see our guide to [CSS custom properties](/css-variables-guide/). ## Patterns for Reusing Variables Well A few habits that separate maintainable variable systems from sprawl: - **Name by role, not appearance.** `$color-danger` survives a redesign; `$red` becomes a lie the day danger turns orange. - **Layer your tokens.** Define raw values first (`$green-600: #1f7a5e;`), then map them to roles (`$color-success: $green-600;`). Components reference roles only. - **Reach for maps when variables multiply.** Ten `$spacing-*` variables are a map wanting to happen. `map.get($spacing, "md")` with a loop generates utility classes in five lines. - **Keep math in variables, not selectors.** `$gutter-half: $gutter / 2` in one place beats repeating the expression — and note that modern Dart Sass prefers `math.div($gutter, 2)` over the `/` operator for division. When a variable starts needing *logic* — computing a contrast color, scaling a size — that's the moment to graduate to a function. Our comparison of [Sass mixins vs functions](/sass-mixins-vs-functions/) covers when each tool earns its keep. ## Common Mistakes to Avoid - **Using a variable before declaring it.** Sass reads top to bottom; a reference above the declaration is a compile error. - **Expecting a global update from a plain local assignment.** Inside a block, `$x: value;` shadows; it doesn't overwrite. Use `!global` or restructure. - **Forgetting interpolation** in selectors, property names, and custom property values: `.icon-#{$name}` works, `.icon-$name` doesn't. - **Skipping `!default` in shared partials.** If teammates can't override your variables at `@use` time, your "design tokens" are hard-coded constants. Master these and variables stop being just a convenience — they become the contract that keeps a whole team's CSS speaking one language. ## Frequently asked questions **How do you declare a variable in SCSS?** Write a dollar sign, a name, a colon, and a value: $brand-color: #1f7a5e;. Then use the variable anywhere a value could appear, such as background: $brand-color;. Declarations at the top level of a file are global and available everywhere after that point; declarations inside a selector, mixin, or function are local to that block. Hyphens and underscores in names are interchangeable, so $font-size and $font_size are the same variable. **What is the difference between Sass variables and CSS variables?** Sass variables are resolved at compile time and disappear from the final CSS — every reference is replaced by its value. CSS custom properties (--name) stay in the compiled stylesheet, participate in the cascade, and can be changed at runtime by JavaScript or per-element overrides. Use Sass variables for build-time values like breakpoints and loop data, and CSS variables for runtime concerns like theme switching. Many projects use both together. **What does the !default flag do in Sass?** The !default flag assigns a value to a variable only if that variable is currently undefined or null; otherwise the assignment is skipped. Library authors mark their variables with !default so consumers can override them, typically at load time with @use 'library' with ($accent: #ff5722). Only variables declared with !default can be configured this way — trying to configure any other variable is a compile error. **How does variable scope work in Sass?** Variables declared at the top level of a stylesheet are global and usable anywhere after the declaration. Variables declared inside a block — a selector, mixin, or function — are local and only exist within that block. Declaring a local variable with the same name as a global one shadows the global inside that block without changing it elsewhere. To deliberately reassign a global from inside a block, add the !global flag. **Can you use a Sass variable inside a CSS custom property?** Yes, but you must use interpolation. Inside a custom property declaration Sass treats the value as opaque text, so --brand: $brand; would output the literal string "$brand". Wrap the variable in interpolation instead: --brand: #{$brand};. This pattern is common for theming — Sass holds the design tokens at build time, and the interpolated custom properties expose them to the browser at runtime. **How do you share Sass variables between files?** Put them in a partial such as _tokens.scss and load it with @use 'tokens'; then reference values through the namespace, like tokens.$brand. You can shorten the namespace with @use 'tokens' as t, or remove it with as *, though the namespace is worth keeping for readability. Avoid the older @import rule for new code — it is deprecated in favor of @use and @forward. --- # Sass vs CSS: What's the Difference and When to Use Sass > CSS is the stylesheet language browsers run natively; Sass is a preprocessor that extends CSS with variables, nesting, mixins, functions, and a module system, then compiles to plain CSS before the browser sees it. Sass doesn't replace CSS — every Sass project ships CSS in the end. Use plain CSS for small projects with no build step; use Sass for large codebases, design systems, and Sass-based frameworks like Bootstrap, where its organization and reusable patterns pay off. Source: https://thesassway.com/sass-vs-css/ Publisher: The Sass Way Published: 2026-08-20 Updated: 2026-08-20 ## Sass vs CSS: The Short Answer CSS is the stylesheet language browsers actually understand. Sass is a preprocessor: a language that extends CSS with variables, nesting, mixins, functions, and a module system, then compiles down to plain CSS before it ever reaches a browser. Sass doesn't compete with CSS — it sits on top of it. Every valid CSS file is also valid SCSS, and every Sass project ships CSS at the end of the day. So the real question isn't "which language should I learn?" You need CSS either way. The question is whether adding a compile step buys you enough maintainability to be worth it — and the answer depends on the size of your project, your team, and how much modern CSS has already caught up. This guide walks through exactly what separates the two, what Sass still does better, and when plain CSS is genuinely the smarter choice. ## What Is CSS? CSS (Cascading Style Sheets) is the standard language for styling web pages. It controls color, typography, spacing, layout, and animation, and it runs natively in every browser — no build tools, no installation, no compile step. You write a `.css` file, link it from your HTML, and you're done. CSS has also evolved dramatically. Custom properties (CSS variables), `calc()`, Grid, Flexbox, container queries, and native nesting have landed in browsers over the past decade, absorbing several features that once required a preprocessor. ## What Is Sass? Sass (Syntactically Awesome Style Sheets) is the most widely used CSS preprocessor. Created by Hampton Catlin in 2006 and developed for years by Natalie Weizenbaum, it adds programming conveniences to stylesheet authoring: variables, nesting, mixins, functions, loops, conditionals, and partial files you can compose into one output. Browsers can't read Sass. A compiler — today that's Dart Sass, the primary implementation — transforms your `.scss` or `.sass` files into ordinary CSS as part of your build. If you're brand new to the language, our [beginner's introduction to Sass](/what-is-sass/) covers the fundamentals in more depth. One point of frequent confusion: Sass has two syntaxes. SCSS uses curly braces and semicolons and is a strict superset of CSS; the older indented syntax drops the punctuation entirely. Almost every modern codebase uses SCSS — the [Sass vs SCSS comparison](/sass-vs-scss/) breaks down why. ## Sass vs CSS: Key Differences at a Glance | Feature | CSS | Sass (SCSS) | |---|---|---| | Runs natively in browsers | Yes | No — compiles to CSS first | | File extension | `.css` | `.scss` (or `.sass`) | | Variables | Custom properties, resolved live in the browser | `$variables`, resolved at compile time | | Nesting | Native nesting in modern browsers | Yes, with the `&` parent selector | | Mixins (reusable style blocks) | No | Yes, `@mixin` / `@include` | | Functions, loops, conditionals | No authoring logic | Yes: `@function`, `@each`, `@for`, `@if` | | Splitting styles into files | `@import` (extra HTTP requests) or bundler | Partials with `@use` / `@forward`, one compiled file | | Build step required | No | Yes | | Errors | Invalid rules silently ignored | Compiler fails loudly with file and line number | ## What Sass Adds That CSS Doesn't Have ### Variables that compile away Sass variables are resolved at compile time and can be used anywhere — including places CSS custom properties can't go, like inside a media query condition: ```scss $tablet: 768px; .sidebar { display: none; @media (min-width: $tablet) { display: block; } } ``` That compiles to: ```css .sidebar { display: none; } @media (min-width: 768px) { .sidebar { display: block; } } ``` CSS custom properties, by contrast, are live values the browser resolves at runtime — which makes them better for theming (think dark mode) but useless for breakpoints. Mature codebases often use both: Sass variables for build-time constants, custom properties for anything that changes in the browser. ### Nesting with the parent selector Sass popularized nesting, and its `&` parent selector remains a workhorse for states and modifiers: ```scss .button { background: #2563eb; &:hover { background: #1d4ed8; } &.is-disabled { opacity: 0.5; } } ``` Compiled output: ```css .button { background: #2563eb; } .button:hover { background: #1d4ed8; } .button.is-disabled { opacity: 0.5; } ``` Modern browsers now support native CSS nesting too, and the two behave similarly but not identically — our guide to [native CSS nesting vs Sass nesting](/css-nesting-vs-sass-nesting/) covers the differences that actually bite. ### Mixins and functions Mixins are reusable blocks of declarations, optionally with arguments. Nothing in CSS does this: ```scss @mixin truncate($lines: 1) { overflow: hidden; text-overflow: ellipsis; @if $lines == 1 { white-space: nowrap; } @else { display: -webkit-box; -webkit-line-clamp: $lines; -webkit-box-orient: vertical; } } .card-title { @include truncate(2); } ``` Add `@function` for computed values and `@if` for conditional logic, and you have a genuine programming layer over your styles. Loops push this further. With `@each`, a whole family of utility classes falls out of one small map: ```scss $spacings: (1: 4px, 2: 8px, 3: 16px); @each $key, $value in $spacings { .mt-#{$key} { margin-top: $value; } } ``` Compiled output: ```css .mt-1 { margin-top: 4px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 16px; } ``` Change the map, and every generated class updates on the next compile. For design-token-heavy systems — spacing scales, color ramps, typography sets — this is where Sass pulls far ahead of hand-written CSS, which would force you to type and maintain each of those rules yourself. ### Partials and the module system Sass lets you split styles into partials (files prefixed with an underscore, like `_buttons.scss`) and compose them with `@use`: ```scss @use "buttons"; @use "forms"; @use "layout"; ``` Everything compiles into a single CSS file, so the browser makes one request no matter how many source files you maintain. Note that Sass's older `@import` rule is deprecated in favor of `@use` and `@forward` — new projects should not use it. Similarly, the classic `darken()` and `lighten()` functions are discouraged; modern Sass recommends `color.adjust()` or `color.scale()` from the built-in `sass:color` module instead. ### Better failure modes When CSS hits an invalid declaration, the browser silently skips it and moves on — a typo can cost you an hour. When Sass hits an error, compilation fails with a file name and line number. On a team, that fast feedback loop is one of the least-advertised but most valuable differences. ## Does Sass Replace CSS? No — and it never will. Sass is a superset of CSS, not a substitute for it. The browser only ever sees the compiled CSS output, and everything you write in Sass ultimately expresses CSS properties and selectors. If your CSS fundamentals are shaky, Sass will happily help you generate bad CSS faster. It helps to picture the workflow. You author `.scss` files, a compiler watches them, and every save produces a fresh `.css` file that your HTML links to exactly as it always has. Your specificity battles, cascade questions, and layout decisions are all still CSS problems; Sass just changes the ergonomics of the authoring step. This is also why learning Sass is easy once you know CSS: rename a `.css` file to `.scss` and it's already valid. You adopt features incrementally — variables first, then nesting, then mixins — without rewriting anything. ## Is Sass Better Than CSS? "Better" is the wrong frame, because they're not competitors. The honest comparison is **Sass + a build step** versus **plain modern CSS**, and each wins in different situations. **Where Sass wins:** - **Large codebases.** Partials, `@use`, and namespacing keep hundreds of components organized in a way vanilla CSS can't match. - **Design systems and tokens.** Loops and maps generate consistent utility classes and themes from a single source of truth. - **Reusable patterns.** Mixins eliminate the copy-paste that plain CSS forces on you. - **Frameworks.** Bootstrap (since version 4) is written in Sass, so customizing its variables properly means working in SCSS. **Where plain CSS wins:** - **Zero tooling.** No installation, no build pipeline, no compile step to babysit. For small sites this is a real advantage, not a consolation prize. - **Runtime dynamism.** Custom properties can change live — respond to user preferences, themes, or JavaScript. Sass variables can't; they're gone by the time the page loads. - **The gap is narrowing.** Native nesting, `calc()`, `color-mix()`, and custom properties have absorbed a meaningful chunk of what people historically installed Sass for. ## When to Use Sass (and When CSS Is Enough) Reach for **plain CSS** when: - The project is small — a landing page, a portfolio, a handful of templates. - You want zero build tooling, or the team isn't comfortable maintaining a pipeline. - Your main needs are variables and nesting, which modern CSS now handles natively. Reach for **Sass** when: - The stylesheet is big enough that file organization is a real problem. - You're building a design system with tokens, generated utilities, or themes. - You rely on mixins, loops, or functions to avoid repetition. - You're customizing a Sass-based framework like Bootstrap. - Your project already has a build step, so compilation costs you nothing extra. A useful rule of thumb: below roughly a thousand lines of styles, the preprocessor's overhead usually outweighs its benefits. Beyond that — especially with multiple contributors — Sass's structure starts paying for itself daily. ## The Bottom Line CSS is the language; Sass is a power tool for writing it. You can't skip CSS, and Sass can't replace it — but on projects with real scale, Sass's variables, nesting, mixins, and module system still deliver an authoring experience plain CSS hasn't fully matched, even as native features close the gap. Learn CSS thoroughly first. Then, when a project grows past what one tidy stylesheet can handle, add Sass deliberately. If you're ready to go deeper, our [complete guide to Sass](/complete-guide-to-sass/) takes you from installation to a scalable file architecture, step by step. ## Frequently asked questions **Is Sass better than CSS?** Neither is better — they solve different problems. CSS is the language browsers understand; Sass is a tool for writing it more maintainably. Sass wins on large projects thanks to partials, mixins, loops, and compile-time variables. Plain CSS wins when you want zero tooling or need runtime features like custom properties that change with themes or user preferences. Modern CSS has also absorbed some classic Sass features, including nesting, which narrows the gap for smaller projects. **Does Sass replace CSS?** No. Sass is a superset of CSS, not a substitute. Everything you write in Sass compiles down to ordinary CSS properties and selectors, and the browser only ever loads that compiled output. You still need solid CSS fundamentals — specificity, the cascade, layout — because Sass changes how you author styles, not how they behave. Any valid CSS file is already valid SCSS, which makes adopting Sass incremental rather than a rewrite. **Do I need to learn CSS before Sass?** Yes, and there's no way around it. Sass ultimately produces CSS, so every property, selector, and layout concept you use in Sass is a CSS concept. If your fundamentals are weak, Sass will only help you generate bad CSS faster. The good news: once you know CSS, learning Sass is quick. SCSS syntax is a strict superset of CSS, so you can rename a .css file to .scss and adopt features like variables and nesting one at a time. **What can Sass do that CSS can't?** Sass offers mixins (reusable blocks of declarations with arguments), functions, loops, and conditionals — none of which exist in CSS. It also provides compile-time variables that work anywhere, including media query conditions, plus a module system with @use and @forward that merges many partial files into one compiled stylesheet. CSS has closed some gaps with custom properties, calc(), and native nesting, but it still has no equivalent to mixins, loops, or Sass's file organization. **Is Sass still worth using now that CSS has variables and nesting?** Often, yes — but less universally than before. Native custom properties and nesting cover the two features people most commonly installed Sass for, so small projects can comfortably skip the build step. Sass still earns its place on large codebases and design systems, where mixins, loops, maps, and the @use module system generate and organize styles in ways CSS can't. If your project already has a build pipeline, adding Sass costs almost nothing. **Do browsers understand Sass files?** No. Browsers only parse CSS, so a .scss or .sass file must be compiled before it can style a page. The standard compiler is Dart Sass, typically run through npm scripts or a bundler like Vite or webpack, and it watches your source files and outputs regular CSS on every save. Your HTML then links to that compiled .css file exactly as it would with hand-written styles — the browser never knows Sass was involved. --- # Sass vs SCSS: What's the Difference and Which Should You Use? > Sass and SCSS are two syntaxes for the same preprocessor, both compiled by Dart Sass into identical CSS. SCSS (.scss) uses braces and semicolons like regular CSS and is a strict superset of it; the indented syntax (.sass) uses indentation and newlines instead. For nearly everyone, SCSS is the better default because it is CSS-compatible and the community standard. Source: https://thesassway.com/sass-vs-scss/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## Sass vs SCSS: the short answer Sass and SCSS are not two different tools. They are **two syntaxes for the same preprocessor**, which is officially called Sass. Both are compiled by the same engine (Dart Sass) and produce identical CSS. The only real difference is how the source code looks: - **SCSS** (`.scss`) uses curly braces `{}` and semicolons `;`, exactly like regular CSS. Any valid CSS file is already valid SCSS. - **The indented syntax** (`.sass`) drops the braces and semicolons, using indentation and newlines to define structure instead. For almost everyone, **SCSS is the right choice** in 2026. It is the syntax used in nearly every tutorial, framework, and codebase, it is a strict superset of CSS so you can paste plain CSS straight in, and the wider community means better editor support and fewer surprises. The indented syntax still works and still ships, but it is a minority style you will rarely meet on a team. ## The same feature in both syntaxes The fastest way to understand the difference is to see one snippet written both ways. Here is a nested rule with a variable and a mixin. **SCSS syntax (`styles.scss`):** ```scss $brand: #2d72d9; @mixin card-padding($size) { padding: $size; border-radius: $size / 2; } .card { color: $brand; @include card-padding(16px); .title { font-weight: 700; } } ``` **Indented syntax (`styles.sass`):** ```sass $brand: #2d72d9 @mixin card-padding($size) padding: $size border-radius: $size / 2 .card color: $brand +card-padding(16px) .title font-weight: 700 ``` Both compile to exactly the same CSS. Notice the differences in the `.sass` version: no braces, no semicolons, indentation defines nesting, and `@include` can be shortened to `+`. The indented syntax also uses `=` as a shorthand for `@mixin` if you want it. > Note: The `$size / 2` division above is shown for illustration. In current Dart Sass, `/` as division is deprecated. For real projects, add `@use "sass:math";` at the top of the file and write `math.div($size, 2)` instead. See the tooling caveats below. ## Side-by-side comparison | Aspect | SCSS (`.scss`) | Indented syntax (`.sass`) | |---|---|---| | Braces and semicolons | Required (like CSS) | Not used | | Structure defined by | `{}` blocks | Indentation | | Valid CSS is valid here | Yes (strict superset) | No — must be rewritten | | Mixin include | `@include name` | `@include name` or `+name` | | Mixin definition | `@mixin` | `@mixin` or `=name` | | Multiline values / long selectors | Straightforward | Trickier, needs care | | Community / tutorial coverage | The default everywhere | Niche | | File extension | `.scss` | `.sass` | | Compiles to CSS via | Dart Sass | Dart Sass | ## Why the two syntaxes exist Sass launched in 2006 with only the indented syntax, heavily inspired by Haml, a terse Ruby templating language. It was deliberately minimal: no braces, no semicolons, less to type. That terseness turned out to be a barrier. Developers coming from CSS had to relearn how to write every rule, and pasting existing CSS never worked. So in 2010, Sass 3 introduced **SCSS** ("Sassy CSS") as a second, CSS-friendly syntax. Because SCSS is a superset of CSS, teams could rename a `.css` file to `.scss` and start adopting Sass features gradually, without rewriting anything. That low barrier is why SCSS became the dominant style and stayed there. Today both syntaxes are first-class citizens in **Dart Sass**, the current and only actively maintained implementation. (The older Ruby Sass and LibSass/node-sass are both deprecated and should not be used on new projects.) ## Which should you use? ### Choose SCSS if… - You want the safest, most transferable default. This covers the vast majority of projects. - Your team or codebase already uses CSS and you want a smooth migration path. - You rely on framework docs, Stack Overflow answers, or design systems — almost all of them are written in SCSS. - You paste CSS from other sources regularly (it just works). ### Consider the indented syntax if… - You strongly prefer whitespace-significant, brace-free code (the Python or Haml aesthetic). - You are working solo or on a codebase that already standardized on `.sass`. - You value keystroke economy over broad familiarity. There is **no performance difference and no feature difference** between them. Everything you can do in SCSS you can do in the indented syntax and vice versa. The decision is purely about readability, team conventions, and ecosystem fit — and on all three, SCSS usually wins. If you are still deciding whether to use Sass at all, our [What Is Sass?](/what-is-sass/) explainer covers the core value proposition, and the [Complete Guide to Sass](/complete-guide-to-sass/) walks through a real setup from scratch. ## Migrating a CSS file to SCSS One of SCSS's biggest practical advantages is that adoption costs almost nothing. Because `.scss` is a superset of CSS, you can turn an existing stylesheet into a Sass file in three steps: 1. **Rename** `styles.css` to `styles.scss`. It already compiles — a plain CSS file is valid SCSS. 2. **Point your build at it.** Run `sass styles.scss styles.css` (or `sass --watch` during development) so the compiled output is what you actually ship to the browser. 3. **Refactor gradually.** Introduce variables, nesting, and mixins one section at a time. Nothing forces you to rewrite the whole file at once. The indented syntax cannot do step one. A raw CSS file pasted into a `.sass` file will fail to compile because of the braces and semicolons, so every rule has to be rewritten before it works. For teams with an existing CSS codebase, that friction alone usually settles the debate. A common workflow is to keep authored SCSS in a `src/` folder and compile it into a `dist/` or `css/` folder that gets deployed. Never edit the compiled CSS directly — your changes would be overwritten on the next build. ## Editor and ecosystem support Both syntaxes are recognized by every major editor, but the depth of support differs: - **Syntax highlighting and IntelliSense** are more mature for `.scss` in VS Code, WebStorm, and Sublime, simply because it is the far more common format. - **Linters** like Stylelint work with both, but most shared configs and rules are authored and tested against SCSS first. - **Framework and library docs** — Bootstrap and countless design systems ship their Sass sources as `.scss`. If you want to import and override their variables, you will be reading and writing SCSS. None of this makes the indented syntax broken; it just means the well-trodden path is SCSS, and staying on it means fewer sharp edges. ## Common misconceptions **"SCSS is newer and better than Sass."** Not quite. SCSS *is* Sass — it is one of Sass's two syntaxes. There is no version of Sass without SCSS support. When people say "Sass vs SCSS," they almost always mean "indented syntax vs SCSS syntax." **"You have to pick one forever."** You are never locked in. Because both syntaxes express the same features, you can rewrite a file from one to the other by hand, and mixing extensions across files in one project is fine — Sass compiles each file according to its extension, so a `.sass` file and a `.scss` file can `@use` each other freely. (Note that Dart Sass does not ship an automated `.sass`↔`.scss` translator; the old Ruby `sass-convert` tool is gone, and `sass-migrator` handles things like `@import`→`@use` upgrades, not syntax conversion.) **"The `.sass` syntax is deprecated."** It is not. The indented syntax is fully supported in Dart Sass and receives the same features as SCSS. It is just less popular. ## Tooling and modern caveats Whichever syntax you pick, a few current realities apply: - **Use Dart Sass.** Install it with `npm install -D sass` or via Homebrew/Chocolatey. LibSass (`node-sass`) is deprecated and misses newer features. - **Prefer `@use` and `@forward` over `@import`.** The old `@import` rule is being phased out of Sass. New code should use the module system, which scopes variables and avoids duplicated output. - **Division uses `math.div()`.** As shown earlier, the `/` operator for division is deprecated. Load the built-in math module with `@use "sass:math";` and call `math.div($a, $b)`. - **Native CSS caught up on some features.** Browsers now support CSS nesting and custom properties natively. If nesting is the only reason you were reaching for Sass, read [Native CSS Nesting vs Sass Nesting](/css-nesting-vs-sass-nesting/) before adding a build step. None of these caveats favor one syntax over the other — they apply equally to `.scss` and `.sass` files, because both run through the same compiler. ## The bottom line Sass is the preprocessor; SCSS and the indented syntax are two ways to write it. They compile to the same CSS and share every feature. Pick **SCSS** unless you have a specific reason not to — it is the CSS-compatible, community-standard default that will serve you well on virtually any project. Reserve the indented `.sass` syntax for situations where you or your team genuinely prefer brace-free, whitespace-driven code. ## Frequently asked questions **Is SCSS better than Sass?** SCSS isn't a competitor to Sass; it's one of Sass's two syntaxes. Compared to the indented .sass syntax, SCSS is usually the better pick because it's a strict superset of CSS, so any valid CSS is valid SCSS, and it's the style used across almost every tutorial, framework, and codebase. Neither syntax is faster or more capable than the other. **What is the difference between Sass and SCSS?** They're two syntaxes for the same preprocessor. SCSS files (.scss) use curly braces and semicolons exactly like CSS, while the indented syntax (.sass) drops both and relies on indentation and newlines. Both support the same features, variables, mixins, nesting, and functions, and both compile to identical CSS through Dart Sass. The choice is purely about how the source code looks. **Should I use .sass or .scss files?** Use .scss for almost every project. It's CSS-compatible, so you can paste plain CSS straight in, and it has far wider community, editor, and framework support. Choose the indented .sass syntax only if you or your team strongly prefer brace-free, whitespace-driven code. There's no performance or feature difference between the two file types. **Do Sass and SCSS compile to the same CSS?** Yes. Both syntaxes are handled by the same compiler, Dart Sass, and produce identical CSS output. A variable, mixin, or nested rule written in .scss and the equivalent written in .sass generate exactly the same result. The syntax you choose affects only the readability of your source files, never the final stylesheet the browser receives. **Is the indented .sass syntax deprecated?** No. The indented syntax is fully supported in Dart Sass, the current and only actively maintained implementation, and it receives the same new features as SCSS. It's simply less popular. What is deprecated is the old Ruby Sass engine and LibSass/node-sass, plus specific rules like @import and using / for division, which affect both syntaxes equally. **Can I convert between Sass and SCSS?** Yes, though it's a manual rewrite rather than a one-click conversion. Because both syntaxes express exactly the same features, translating a file between .sass and .scss is mechanical: add or remove braces and semicolons. Dart Sass does not ship an automatic syntax translator (the old Ruby sass-convert tool is retired, and sass-migrator only handles upgrades like @import to @use), but you don't have to convert anything anyway — you can mix both extensions in one project, since each file is compiled according to its extension and can @use the others freely. --- # What Is CSS? A Beginner's Guide to Styling the Web > CSS (Cascading Style Sheets) is the language that controls how a web page looks. HTML defines a page's content and structure, while CSS decides its presentation: colors, fonts, spacing, sizing, and layout. It works by pairing selectors, which choose elements, with declarations that style them, and runs natively in every browser. Source: https://thesassway.com/what-is-css/ Publisher: The Sass Way Published: 2026-07-19 Updated: 2026-07-21 ## What Is CSS? The Short Answer **CSS (Cascading Style Sheets) is the language that controls how a web page looks.** HTML defines the content and structure of a page (headings, paragraphs, images, links), and CSS decides how that content is presented: colors, fonts, spacing, sizing, and layout. Where HTML says "this is a heading," CSS says "make that heading dark blue, 32 pixels, and centered." Every styled website you visit relies on CSS. It runs natively in every browser, requires no plugins or build tools to get started, and works alongside HTML and JavaScript as one of the three core technologies of the web. The word **cascading** is the key idea: when several style rules target the same element, CSS follows a defined set of priority rules to decide which one wins. That system is what makes CSS both powerful and, occasionally, confusing. ## What Does CSS Stand For? CSS stands for **Cascading Style Sheets**. Breaking that down: - **Cascading** — Rules "cascade" from multiple sources (browser defaults, your stylesheets, inline styles) and combine according to a priority order. When two rules conflict, the cascade decides the winner. - **Style** — CSS describes visual style: color, typography, spacing, borders, backgrounds, animation, and layout. - **Sheets** — Rules are typically collected in a separate `.css` file (a "style sheet") so one file can style an entire site. CSS was first proposed by Håkon Wium Lie in 1994 and became an official W3C standard in 1996. Today it is maintained as a set of modular specifications by the W3C's CSS Working Group, with new features shipping in browsers regularly. ## What Is CSS Used For? CSS handles everything about how a page presents itself. In practice, developers use it for: - **Colors and backgrounds** — text color, background colors, gradients, and images. - **Typography** — font family, size, weight, line height, and letter spacing. - **Spacing and sizing** — the margins, padding, borders, and dimensions of every element, governed by the [CSS box model](/css-box-model/). - **Layout** — arranging elements into columns, grids, and responsive structures using Flexbox and CSS Grid. - **Responsive design** — adapting a single layout to phones, tablets, and desktops with media queries. - **Motion and effects** — transitions, keyframe animations, shadows, and transforms, often with no JavaScript at all. - **Interaction states** — hover, focus, and active styles that give users visual feedback. Without CSS, every website would look like an unstyled document: black text, blue links, Times New Roman, stacked top to bottom. CSS is what turns that raw content into a designed experience. ## How CSS Works: Rules, Selectors, and Declarations A CSS **rule** has two parts: a **selector** that picks which elements to style, and a **declaration block** of property-value pairs that describe how to style them. ```css h1 { color: navy; font-size: 2rem; text-align: center; } ``` Reading that rule: - `h1` is the **selector** — it targets every `

` element on the page. - Everything inside the `{ }` is the **declaration block**. - `color`, `font-size`, and `text-align` are **properties**. - `navy`, `2rem`, and `center` are **values**. - Each property-value pair is a **declaration**, ended with a semicolon. You can target elements in many ways. The most common selectors: | Selector | Example | Targets | |---|---|---| | Type | `p` | All `

` elements | | Class | `.card` | Elements with `class="card"` | | ID | `#header` | The element with `id="header"` | | Descendant | `nav a` | `` elements inside a `