CSS and SCSS
What CSS and SCSS are and what role they play in the frontend
CSS as the presentation layer
CSS (Cascading Style Sheets) controls how a web document looks: colours, typography, spacing, layout, how the page responds to screen size and, more and more often, simple visual behaviour too. HTML describes the structure of the content, JavaScript handles the logic, and CSS tells the browser how all of it should look. The W3C describes CSS as a mechanism for adding style to web documents, and that is still the best short definition, even though the language itself has grown a great deal.
“Cascading Style Sheets (CSS) is a simple mechanism for adding style to Web documents.”
W3C
In practice CSS works through selectors, declarations and the cascade. A rule such as .button { color: red; } reaches the browser, is parsed, matched against the document tree and affects the final look of the page. It sounds simple. Things get harder with specificity, inheritance, the order in which files load and the global scope of selectors. That is why in larger applications we don’t treat CSS as “the file with the colours”, but as a separate layer of the frontend architecture.
SCSS as a Sass syntax
SCSS is the most widely used syntax of Sass, a CSS preprocessor. The Sass documentation describes it as a superset of CSS: in principle, a valid CSS file can be saved as .scss and the compiler should understand it. This matters, because moving to SCSS doesn’t require rewriting the whole styling layer from scratch. You can start with ordinary rules and only later add variables, nesting, mixins, modules and functions.
The browser doesn’t run SCSS natively. It only receives the result of compilation, which is plain CSS. This step can be handled by the Sass CLI, Vite, Webpack, Parcel or another frontend build tool. For someone managing a project, this means one thing: SCSS gives you better-organised source code, but in production what counts is still the quality of the generated CSS. If we write overly deep selectors in SCSS, the browser won’t say “that came from a preprocessor, so I’ll let it go”. It will simply receive a heavy stylesheet.
This is where the biggest trade-off lies: SCSS brings order to developers’ work, but it adds a build step and can hide complexity if nobody keeps an eye on the final output.
A short history of CSS: from simple styles to large UI systems
Why CSS was created alongside HTML
In the early days the web was more a collection of documents than of applications. HTML carried the content: headings, paragraphs, lists, tables. The problem appeared when appearance started to mix with structure. Presentational attributes, tables used for page layout, colours and fonts repeated by hand: all of this worked while a site was small. After that, every redesign meant digging through many places at once.
CSS separated content from presentation. That was a big step, because it made it possible to change the look without touching the HTML. One class could serve many elements, and one stylesheet could style many pages. From today’s frontend perspective this sounds obvious, but this separation is what the whole later way of working grew out of: components, design systems, themes, responsive design and styles shared between views.
From CSS versions to modules
CSS no longer develops like a classic product with “versions”, where everyone waits for one big bundle of features. Today’s CSS is developed in modules. Selectors, colours, layouts, container queries, the cascade, transforms and animations each mature separately. This makes sense, because browsers can roll out specific modules gradually, and standards authors can refine part of the language without holding up everything else.
This model does have consequences, though. A feature may be described in the specification, partly available in one browser, hidden behind a flag in another and still missing in a third. That’s why in frontend projects we check browser support, not just whether the syntax looks elegant in the documentation. The standard is one thing. A user with a specific browser version is another.
Why preprocessors were needed
Sass and SCSS appeared as a response to the limitations of older CSS. No variables, no nesting, no sensible way to split code up, colour and spacing values repeated everywhere: all of this was painful, especially on larger websites. A preprocessor made it possible to write styles more like code, with modules, parameters, functions and shared definitions.
“Sass is CSS with superpowers: the most mature, stable and powerful professional grade CSS extension language.”
Sass documentation
Today the situation is more interesting. CSS has native variables, nesting, cascade layers, container queries and increasingly good scoping mechanisms. SCSS hasn’t disappeared, but its role has changed: less “patching gaps in the language”, more organising a large codebase of styles. The limitation? The more logic we move into the preprocessor, the easier it is to forget that in the end everything turns into plain CSS.
Browsers, standards and rendering: why CSS behaves differently in practice
A standard doesn’t render the page
CSS is described by standards, including those developed at the W3C, but a page is rendered by a specific browser: Chrome, Safari, Firefox, Edge or an engine embedded in a mobile app. This is an important distinction. The specification describes the expected behaviour, conformance tests help to verify it, and browser makers implement features in their own engines. That’s why a new CSS property can sometimes already be “standard” but still not safe to use in production without checking support.
In practice we look at browser support (browser support), behaviour on the target devices and whether a safe fallback can be used. Tools such as Autoprefixer can add vendor prefixes where that makes sense, but they won’t replace an architectural decision. If a feature doesn’t exist in a given browser, a prefix won’t work magic.
The CSSOM and the road to pixels
When a browser loads a page, it builds the DOM tree from the HTML and the CSSOM from the stylesheets. The CSSOM (CSS Object Model) contains the processed style rules. The browser then combines the DOM and the CSSOM, calculates the styles for each element, works out the layout, paints and composites the layers on screen. This is a simplification, but it’s enough to understand why styling affects performance.
Changing a class on an element can trigger a recalculation of styles. Changing a width can force the layout to be recalculated. Animating properties that affect geometry tends to be more expensive than transforms or opacity. That’s why in interfaces with many elements we avoid needlessly complex selectors, accidental rule overrides and animations that force the browser to keep working on the layout.
Why “it works on my machine” isn’t enough
Differences between browsers don’t come only from bugs. Sometimes a feature is new. Sometimes the interpretation of the specification has subtleties. Sometimes the problem is a combination of several mechanisms: the cascade, inheritance, default form styles and system styles. Forms are a classic example: the same select can look different in different environments, even with correct CSS.
So in frontend projects we plan visual and technical testing. We check the most important views in the target browsers, not only in the one the developer happens to use. With new CSS features we take a progressive approach: first solid baseline behaviour, then a better experience where the browser supports it. The downside of this approach is the extra discipline it requires in the code. The upside: fewer surprises after launch.
What SCSS gives you: variables, nesting, mixins and code organisation
The most important SCSS features
SCSS provides tools that CSS lacked for years, or that have only recently appeared natively. SCSS variables let you keep values such as colours, spacing or breakpoints in one place. Nesting helps you write styles that follow the structure of a component. Mixins are useful for repeated sets of declarations, for example responsive variants or shared typography styles. Modules keep imports in order and reduce the mess familiar from old files with one huge collection of rules.
Good SCSS, however, isn’t about using every feature at once. Usually the simple decisions bring the most value: splitting files by responsibility, consistent naming, limiting nesting and a clear place for variables. Deep selectors such as .page .section .card .header .title look neat in SCSS because they’re nicely indented. After compilation they turn into heavier, more specific CSS that is harder to override.
Compilation matters
Sass compiles SCSS into plain CSS. This means you have to keep an eye on the output: the size of the stylesheets, repeated declarations, the order of rules, source maps and how files are split. During development a source map helps you find the SCSS line responsible for a rule you see in the browser’s developer tools. In production we usually want a minified file, without unnecessary comments or dead rules.
SCSS works well with a component-based architecture if we treat it as the source layer and not as a place to dump everything. You can keep styles close to components and separate out global foundations: reset, typography, grid, variables and utilities. In applications with a long lifespan we often choose a compromise: native CSS variables for values that change while the page is running, and SCSS variables for things compiled once, such as spacing scale maps.
The limitation is simple: SCSS won’t fix a bad architecture. It gives you better tools, but badly named classes, global overrides and a lack of organising rules will still come back, just in a nicer syntax.
CSS/SCSS vs Sass vs CSS-in-JS vs utility-first CSS: when to choose which approach
There is no single winner
The right approach to styling depends on how the frontend is built, how often the interface changes and what role the design system plays. Plain CSS is much more powerful today than it was a few years ago. SCSS still helps to organise large stylesheets. CSS-in-JS, meaning styles defined from JavaScript, fits strongly component-based interfaces well, especially when styles depend on a component’s state. Utility-first CSS, an approach based on small utility classes, shortens the path from UI design to code, but it needs consistency across the whole design and development team.
We take a fairly pragmatic view of this. If an application has a classic frontend, many content views and moderate interactivity, CSS or SCSS with a good naming methodology is often the most readable option. If the interface is made up of many self-contained components, it’s worth considering component styles, CSS Modules or locally scoped classes. If the priority is to assemble screens quickly from consistent design tokens, Tailwind CSS can be a good choice, as long as it doesn’t turn the HTML into a wall of random classes.
| Approach | Where it fits best | Strengths | Typical limitations |
|---|---|---|---|
| Plain CSS | Websites, applications with a simpler UI layer, the foundation of a styling system | Native browser support, no build step, more and more modern features | Global scope, risk of conflicts, needs discipline in naming |
| SCSS / Sass | Larger stylesheets, projects that need modules and repeated patterns | Variables, mixins, nesting, convenient organisation of source code | Extra build step, can generate overly complex CSS |
| BEM and CSS methodologies | Interfaces maintained over a long time with many shared components | Predictable class names, fewer accidental overrides | Longer names, rules must be applied consistently |
| CSS-in-JS | Component-based applications, styles that depend heavily on component state and props | Styles kept local, easier to link component logic with appearance | Greater dependence on a library, possible impact on rendering and tooling |
| Utility-first CSS / Tailwind CSS | Building consistent views quickly with utility classes | Little custom CSS, works well with spacing and colour scales | Long class attributes, needs well-documented working rules |
Maintenance matters more than fashion
The most common mistake is choosing a tool because it’s trendy rather than because it suits how the project will be maintained. Tailwind CSS can be excellent in a project with well-defined tokens and repeatable components. Elsewhere, plain SCSS with BEM will be easier to understand and easier for a new team to take over. CSS-in-JS keeps styles local, but it moves part of the responsibility into the JavaScript ecosystem, which you have to take into account for SSR, testing and optimisation.
In styling architecture, what counts is something less exciting: predictability. Is it clear where to change a button’s colour? Is a component variant created by adding a class, changing a token or adding a condition in JavaScript? Does a new view use existing rules, or does it create yet another exception? The answers to these questions usually say more than the name of the technology.
The trade-off is that the most flexible approaches need the most rules. Without rules, CSS becomes global, SCSS becomes too clever, and utility classes start to look like styling by hand in the HTML.
Limitations of CSS and SCSS: where problems appear in larger projects
The cascade is powerful. Sometimes too powerful
The cascade in CSS is both a brilliant idea and the source of many visual bugs. A rule can apply because it is more specific, because it loaded later, because it inherited a value or because an overly broad selector caught the element. On a small site you can manage this “by eye”. In an application with many views, that approach quickly ends with people adding !important, which is usually a sign that the styling architecture has started to crack.
SCSS doesn’t remove this problem, because after compilation we still get CSS. If the source files contain global selectors, overly deep nesting and a random import order, the resulting stylesheet will keep all of these problems. A preprocessor can even make them easier to hide, because the code looks tidier than its compiled version.
Technical debt in styles
Debt in CSS builds up quietly. First someone adds an exception for one view. Then a second exception for the mobile version. Then a button variant that differs by just one value but gets its own class. After a while nobody knows whether a given rule can be removed, because there is no clear link between the style and the component. This isn’t a cosmetic problem. It affects how quickly the product can change.
That’s why in frontend projects we organise styles the same way as application code: naming, scope of responsibility, layers, base components and rules for exceptions. Tools for finding dead CSS, code reviews, visual tests and component documentation all help. It isn’t about bureaucracy. It’s about making sure that changing one element doesn’t set off a domino effect in three other places.
Performance and accessibility
CSS also affects performance and accessibility. Overly heavy stylesheets slow down loading, complex animations can strain weaker devices, and badly written focus styles can make an application hard to use for people who navigate with a keyboard. Hiding an element with display: none, visibility: hidden or by moving it off screen are not equivalent decisions as far as assistive technologies are concerned.
New features such as @scope show where CSS is heading: less global chaos and more control over scope. The limitation, however, remains organisational. Even the best standard won’t replace the decision that styles are part of the architecture, not an add-on at the end of the build.
The future of CSS: native features, design tokens and less reliance on tools
Native CSS is catching up
CSS is moving towards features that were once mainly associated with preprocessors or libraries. CSS nesting lets you write rules that follow the structure of a component. Container queries let you respond to the size of a container, not just the whole browser window. Cascade layers help control the order of priority between groups of rules. @scope develops the idea of limiting the scope of styles, one of the oldest problems in CSS.
This doesn’t mean SCSS no longer makes sense. Rather, the boundary is shifting. Some things that once required a preprocessor can now be done natively. On the other hand, SCSS is still handy where we need to generate variants, maps of values, well-organised modules and a consistent way of working with a large codebase of styles. Because SCSS is syntactically compatible with CSS, the move to newer native features can be planned gradually.
Design tokens and the design system
Design tokens are playing a growing role: named values that describe colour, typography, spacing, corner radii or shadows. In CSS the natural home for tokens is CSS variables, for example --color-primary or --space-md. Their advantage over SCSS variables is that they exist while the page is running. They can be changed through a theme, a class on a parent element, user settings or the component’s context.
In a well-designed system, styles aren’t a collection of random values but a layer of design decisions. A button doesn’t have a “blue background”, it uses the primary action token. A card doesn’t have “24 pixels of spacing”, it uses the spacing scale. This kind of abstraction feels dull until you need to change the theme or tidy up the interface after several product iterations.
How to write styles that will last for years
The safest direction is code that isn’t locked into a single tool. We use standard CSS where it already offers enough. We use SCSS where it genuinely improves organisation and doesn’t create excessive complexity. We introduce design tokens, limit global selectors, document components and check browser support for new features before using them in key views.
The trade-off? Modern CSS requires you to keep your knowledge up to date. It is no longer a language you can learn once and treat as a closed set of properties. For a project that’s good news: more is possible natively in the browser. For the architecture, it’s a signal that it’s worth reviewing the styling layer from time to time instead of adding yet more workarounds.








