Sass in React
Sass (Syntactically Awesome Style Sheets) is a CSS preprocessor that adds variables, nesting, mixins, and file splitting to plain CSS. It compiles down to ordinary CSS before your app ever reaches the browser, so it works with React the same way it works with any project — you write .scss files, your build tool compiles them, and the resulting classes are applied through the normal className prop. Sass is popular in React apps because it keeps large stylesheets organized and lets you avoid repeating the same colors, spacing values, and button styles across dozens of components.
Overview / How it works
React itself has no opinion about how you write CSS. It only cares that a component’s JSX ends up with a className string. Sass fits into this picture entirely at build time: tools like Vite and Create React App bundle a Sass compiler (via the sass npm package) that watches for .scss/.sass files, compiles them into plain CSS, and injects that CSS into the page. By the time your component renders in the browser, there is no Sass left — only the compiled CSS and the class names you reference.
There are two common ways to bring Sass into a React project:
- Global Sass files — a
.scssfile imported once (often inmain.jsxor a component) whose class names are plain global CSS classes, just like a regular.cssfile. - Sass Modules — files named
*.module.scss. The build tool automatically scopes every class name to the component that imports it (similar to CSS Modules), generating unique hashed names likecard_a3f1xbehind the scenes. You import the file as a JavaScript object and reference classes as object properties, e.g.styles.card.
Sass Modules are strongly preferred for component styling because they eliminate the single biggest problem with CSS at scale: global class name collisions. Two components can both define a class named .title without either one leaking into the other.
Because everything happens at build time, Sass has zero runtime cost and no effect on React’s render cycle. Nesting, variables, and mixins are all expanded into flat CSS rules before shipping; React only ever sees and toggles plain string class names, exactly as it would with hand-written CSS.
Setup
To use Sass in a React project (Vite or Create React App), install the Sass compiler as a dev dependency:
npm install --save-dev sass
No extra configuration is required — both Vite and Create React App automatically detect the sass package and know how to compile .scss and .module.scss files the moment you import one.
Syntax
The general shape of using Sass in a component:
// Global stylesheet
import "./Component.scss";
// Scoped Sass Module
import styles from "./Component.module.scss";
| Form | File name | Class access | Scoping |
|---|---|---|---|
| Global Sass | Component.scss |
className="card" |
Global (leaks across files) |
| Sass Module | Component.module.scss |
className={styles.card} |
Locally scoped, hashed class names |
- Variables (
$primary-color: #4f46e5;) store reusable values. - Nesting lets child selectors live inside a parent rule, mirroring your markup structure.
- Mixins (
@mixin/@include) bundle reusable groups of declarations, optionally with arguments. - Partials are files prefixed with an underscore (
_variables.scss) that are meant to be pulled into other files with@use, not compiled on their own. - @use is the modern way to pull in another Sass file (the older
@importis deprecated in Sass).
Examples
Example 1: A global Sass stylesheet
// Counter.scss
$primary-color: #4f46e5;
$spacing: 12px;
.counter {
display: flex;
align-items: center;
gap: $spacing;
padding: $spacing * 2;
border: 2px solid $primary-color;
border-radius: 8px;
&__label {
font-weight: bold;
color: $primary-color;
}
&__button {
background: $primary-color;
color: white;
border: none;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.85;
}
}
}
// Counter.jsx
import { useState } from "react";
import "./Counter.scss";
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter">
<span className="counter__label">Count: {count}</span>
<button className="counter__button" onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
export default Counter;
What it renders: a bordered flex row with a bold indigo label reading “Count: 0” and a solid indigo “Increment” button. Clicking the button calls setCount, React re-renders, and the label updates to show the new count. The &__label and &__button nesting compiles to .counter__label and .counter__button — ordinary global classes, since this file has no .module in its name.
Example 2: Scoped styles with a Sass Module
// Card.module.scss
$radius: 10px;
.card {
border-radius: $radius;
padding: 16px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
&.highlighted {
border: 2px solid #f59e0b;
}
}
.title {
font-size: 1.25rem;
margin-bottom: 8px;
}
// Card.jsx
import styles from "./Card.module.scss";
function Card({ title, children, highlighted = false }) {
const cardClass = highlighted
? `${styles.card} ${styles.highlighted}`
: styles.card;
return (
<div className={cardClass}>
<h3 className={styles.title}>{title}</h3>
<p>{children}</p>
</div>
);
}
export default Card;
What it renders: a padded, shadowed card with a bold title. Because this file ends in .module.scss, the build tool rewrites .card and .title into unique hashed class names (something like Card_card__x1a2b) that only this component can reach through the imported styles object. If highlighted is true, the compiled .highlighted class is appended, adding the orange border defined in the nested &.highlighted rule. Another component can freely define its own .card class without any conflict.
Example 3: Partials and mixins for a themed Button
// _variables.scss
$primary: #2563eb;
$danger: #dc2626;
$radius: 6px;
// _mixins.scss
@mixin button-variant($bg) {
background-color: $bg;
color: white;
border: none;
border-radius: $radius;
padding: 8px 16px;
cursor: pointer;
&:hover {
filter: brightness(0.9);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Button.module.scss
@use "./variables" as *;
@use "./mixins" as *;
.primary {
@include button-variant($primary);
}
.danger {
@include button-variant($danger);
}
// Button.jsx
import styles from "./Button.module.scss";
function Button({ variant = "primary", children, ...rest }) {
const variantClass = variant === "danger" ? styles.danger : styles.primary;
return (
<button className={variantClass} {...rest}>
{children}
</button>
);
}
export default Button;
What it renders: a blue button by default, or a red button when variant="danger" is passed, both sharing the same padding, radius, hover-dim, and disabled styling defined once in the button-variant mixin. The _variables.scss and _mixins.scss partials are never compiled on their own — the leading underscore tells Sass they only exist to be pulled into other files with @use. This pattern (variables + mixins + a module file that assembles them) is how larger React apps keep a consistent design system across many components without repeating values.
How it works step by step / Under the hood
- Build time: when your bundler encounters
import "./Card.module.scss", it hands the file to the Sass compiler. Variables are substituted, nesting is flattened into standard selectors, and@includeexpands each mixin’s declarations inline. The result is plain CSS. - Module scoping: for
.module.scssfiles, the loader additionally renames every class (e.g..card→.Card_card__x1a2b) and replaces the JSimport styles from ...statement with a plain object mapping your original names to the generated ones. - Bundling: the compiled CSS is either injected into a
<style>tag at runtime (dev mode) or extracted into a static.cssfile linked in your HTML (production build). - Render: React only ever sees the final string values (
"Card_card__x1a2b"). Applying, changing, or removing a class is exactly as cheap as it is with hand-written CSS — togglingclassNamebased on state does not re-run Sass or recompile anything; the CSS was already generated once at build time. - Unmount: nothing special happens to Sass-derived styles on unmount. The stylesheet stays loaded in the document; only the DOM nodes referencing those classes are removed.
Common Mistakes
Mistake 1: Accessing a hyphenated class name with dot notation
// Wrong
import styles from "./Card.module.scss";
function Card() {
return <div className={styles.card-title}>Product</div>;
}
Why it’s wrong: styles.card-title is parsed by JavaScript as styles.card - title, a subtraction expression, not a property lookup. Since title isn’t a defined variable, this throws a ReferenceError (or silently evaluates to NaN if it happened to be defined). Any class name containing a hyphen cannot be accessed with dot notation.
// Corrected
import styles from "./Card.module.scss";
function Card() {
return <div className={styles["card-title"]}>Product</div>;
}
Bracket notation works with any string key. An even cleaner fix is to avoid hyphens in Sass Module class names entirely — name the class .cardTitle in the .scss file so styles.cardTitle works directly.
Mistake 2: Expecting a plain .scss import to be automatically scoped
// Wrong: Button.scss (no ".module" in the filename)
.btn {
padding: 8px 16px;
border-radius: 4px;
}
Why it’s wrong: without .module in the filename, the build tool treats this as a global stylesheet. If a second component elsewhere also defines .btn, whichever file is imported last on the page wins, silently overriding the first component’s button styling.
// Corrected: rename to Button.module.scss
import styles from "./Button.module.scss";
function Button({ children }) {
return <button className={styles.btn}>{children}</button>;
}
Renaming the file to end in .module.scss and importing it as an object gives every component’s .btn its own hashed, collision-free class name.
Mistake 3: Trying to change a Sass variable at runtime
Sass variables ($primary-color) only exist while the stylesheet is compiling — they are baked into fixed CSS values before your app ships. Code that tries to reassign a Sass variable from a React event handler does nothing, because there is no Sass compiler running in the browser. For values that need to change at runtime (like a user-selected theme color), use CSS custom properties (--primary-color) set via inline styles or a class toggle, and reference them from Sass with var(--primary-color) instead of a $variable.
Best Practices
- Default to
*.module.scssfor component-level styles so class names never collide across components. - Reserve a single global
.scssfile (e.g.index.scss) for resets, typography defaults, and CSS variables shared app-wide. - Keep shared values in partials (
_variables.scss,_mixins.scss) and pull them in with@use, not the deprecated@import. - Name Sass Module classes in camelCase (
.cardTitle) so they can be accessed asstyles.cardTitlewithout bracket notation. - Keep nesting shallow (2–3 levels); deeply nested selectors produce brittle, hard-to-override CSS.
- Use CSS custom properties, not Sass variables, for any value that needs to change at runtime (themes, user preferences).
- Co-locate a component’s
.module.scssfile in the same folder as its.jsxfile so the relationship is obvious.
Practice Exercises
- Create a
Badge.module.scssfile with a.badgeclass and a$colorsmap forsuccess,warning, anderrorvariants. Build aBadgecomponent that accepts astatusprop and applies the matching color. - Write a
_mixins.scsspartial containing a@mixinfor a flex-centered container (display: flex; align-items: center; justify-content: center;). Use@useand@includeit inside two different.module.scssfiles. - Take the global
Counter.scssfrom Example 1 and convert it toCounter.module.scss. UpdateCounter.jsxto import the file asstylesand reference each class through thestylesobject instead of a string.
Summary
- Sass compiles to plain CSS at build time; React only ever sees the final class name strings, with zero runtime cost.
- Global
.scssfiles produce ordinary, unscoped CSS classes just like a regular stylesheet. *.module.scssfiles are automatically scoped per component — import them as an object (import styles from "./X.module.scss") and reference classes asstyles.className.- Partials (
_variables.scss,_mixins.scss) hold shared variables and mixins, pulled into real stylesheets with@use. - Hyphenated class names need bracket notation (
styles["my-class"]); prefer camelCase class names to avoid this entirely. - Sass variables are fixed at build time — use CSS custom properties for values that need to change while the app is running.
