7 ways to deal with CSS
Based on Fireship's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Global CSS scales poorly due to cascading side effects, naming collisions, and frequent reliance on `!important`, which also makes maintenance harder.
Briefing
CSS stops being “just fundamentals” the moment a real app grows: naming collisions, cascading side effects, and bloated stylesheets quickly become painful. With Next.js and React, there’s no single universal fix—so the practical answer is choosing among several approaches and accepting their trade-offs.
The most basic option is global CSS, where styles apply across the entire app. It can work for small projects, but it scales poorly. Cascading makes class naming harder, often pushing developers toward conventions like BEM and frequent use of overrides such as `!important`. Global styles also tend to inflate bundle size, which can slow page loads.
Next.js offers a more scalable default: CSS Modules. These look like regular CSS but scope class names to a specific component, preventing collisions when multiple components define the same class. Because Next.js can load only the CSS needed for a given page, it can reduce the amount of CSS shipped to the browser. When sharing styles becomes necessary, Modules support a `composes` property to import and override rules from other modules.
If the goal is more “programming-like” CSS, preprocessors such as SCSS add variables, mixins, and functions by compiling to plain CSS. SCSS is a superset of CSS, so developers can keep familiar syntax while gaining concision and reuse. The downside is that it introduces a separate language layer and remains decoupled from the application’s JavaScript logic.
That gap is where CSS-in-JS libraries come in—tools like styled components, emotion, jss, and styletron—letting developers write CSS inside JavaScript. This unlocks dynamic styling tied to application state and gives access to full JavaScript capabilities. A homegrown example mentioned is `style.jsx`, which uses a `style` tag with a `jsx` attribute and template literals to compute/interpolate style values directly. Like CSS Modules, these styles are scoped to where they’re defined, reducing unintended bleeding.
For faster UI building, utility-first styling is another route. Tailwind (and Windy CSS) provides large sets of utility classes so developers can assemble designs quickly, often with IDE autocomplete. Tailwind also supports purging unused CSS to keep bundles lean. The trade-off is that component markup can become cluttered, and the approach requires committing to a particular organization style; it also doesn’t ship pre-built UI components, so more work may be needed.
Finally, there are component-oriented frameworks and design systems. Bootstrap and Bulma provide pre-built components like buttons and cards with minimal setup, but importing their styles can include lots of unused CSS unless additional steps are taken. A more integrated alternative is full component libraries such as Mantine, which combines styling with utilities and even JavaScript-adjacent features (e.g., hooks for the Intersection Observer API, plus tools for modals, notifications, and calendars). These systems are often opinionated, so picking one that fits a team’s preferences matters.
The overall takeaway: the best CSS strategy in React/Next.js is often a mix—CSS Modules for scoping, SCSS or CSS-in-JS for reuse and dynamism, Tailwind for speed, and component libraries for ready-made UI and richer behavior.
Cornell Notes
CSS becomes harder as apps scale because global styles create naming conflicts, cascade surprises, and large CSS bundles. Next.js’s CSS Modules solve much of that by scoping class names per component and loading only the CSS needed for each page, with `composes` for sharing styles. SCSS adds variables, mixins, and functions via compilation, but it’s a separate language layer from the app’s JavaScript. CSS-in-JS libraries (and examples like `style.jsx`) let styles depend on React state using JavaScript features, while still aiming to avoid style leakage through scoping. Utility-first (Tailwind/Windy CSS) speeds UI assembly and can purge unused CSS, but can make markup messy; component frameworks (Bootstrap/Bulma) and design systems (Mantine, Ant, Material, Rebase, Chakra, Tamagui) trade flexibility for ready-made components and integrated behavior.
Why does global CSS become a problem in larger React/Next.js apps?
What does CSS Modules change, and how does Next.js benefit?
How do SCSS preprocessors improve CSS, and what’s the cost?
What advantage do CSS-in-JS libraries provide for dynamic styling?
When is Tailwind (or Windy CSS) a good fit, and what trade-off comes with it?
How do component frameworks and design systems differ from utility-first and CSS Modules?
Review Questions
- Which problems does CSS Modules address compared with global CSS, and what mechanism helps with style reuse?
- In what situations would SCSS be preferable to CSS-in-JS, given the trade-off between concision and JavaScript integration?
- How do Tailwind’s purge capabilities and utility-class markup trade-offs affect maintainability and performance?
Key Points
- 1
Global CSS scales poorly due to cascading side effects, naming collisions, and frequent reliance on `!important`, which also makes maintenance harder.
- 2
CSS Modules scope styles per component and let Next.js load only the CSS needed for each page, reducing bundle size.
- 3
SCSS improves CSS ergonomics with variables, mixins, and functions, but it adds a separate language layer compiled into plain CSS.
- 4
CSS-in-JS enables dynamic, state-driven styling by using JavaScript features directly inside style definitions, often with scoping to prevent leakage.
- 5
Utility-first styling with Tailwind can speed UI development and purge unused CSS, but it can make component markup harder to read.
- 6
Bootstrap/Bulma offer pre-built components with a low learning curve, though they can increase bundle size by including unused styles.
- 7
Full design systems like Mantine provide both styling and higher-level UI behavior, but they are opinionated and require a good fit with team preferences.