Key Takeaways
- CSS frameworks offer short-term gains in speed and consistency but become increasingly hard to maintain over time.
- A codebase that uses a CSS framework will gradually build its own custom framework on top of it. This framework will be difficult to use, understand, and modify.
- CSS frameworks might be a good choice for an app that will fully adopt the framework’s design system with no changes. This sounds good in theory, but it doesn’t happen in practice.
- Write your styles in CSS rather than a compile-to-CSS option like SCSS or JS-to-CSS. The added complexity is high, and the benefit is low due to excellent features in modern CSS.
- Start with semantic styles when building your CSS. This will express your intent to other developers and give them more choice in picking a templating strategy.
Developers use CSS frameworks like Material UI, Bootstrap, or Pico to reduce boilerplate, increase quality, and drive consistency. However, these gains are hard to maintain as an application’s codebase matures. The app’s look and feel evolve away from the framework, new components are added, and existing layouts and components are modified. Developers must configure and override the framework to accommodate these changes. It becomes more difficult to override the framework than to implement the changes from scratch.
Instead of using a CSS framework, I recommend developers write their own custom CSS. As an application's requirements evolve, developers modify existing styles or copy in new styles from a starter rather than override existing styles. Modern CSS has many features that make it possible to write maintainable styles. Keeping styles in the codebase rather than as an external dependency keeps the CSS codebase lean and understandable over time.
Downsides of CSS Frameworks
Overriding
Overriding a framework is time-consuming, difficult to maintain, and error-prone. Frameworks offer the most significant benefit when developers stay within their prescribed bounds. Many offer the ability to customize to some degree, but an application’s customization needs often exceed the framework’s built-in customization options. Developers must become experts in overriding the framework rather than experts in how to use CSS. Overrides often use features that are not part of the framework’s public API, making customizations prone to breaking when upgrading the framework.
Before too long, the framework overrides become so extensive that teams end up with their own custom framework consisting of many overrides, customizations, and extensions. This custom framework uses its own conventions and is challenging to keep maintainable. It often looks foreign, even to developers who are experts in the underlying CSS framework. Problems that would be simple to solve with pure CSS become problematic because they must be solved in the context of the framework.
Hard to Enforce Consistency
Teams sometimes use a CSS framework because the entire product team has committed to using the framework’s design system and never deviating. Many teams start with this goal, but almost none follow it for a significant time. Frameworks’ design systems are incredibly general; they attempt to meet most of the needs of most applications rather than all of the needs of any one application. Over time, an application’s design needs will always deviate from what the framework provides.
Difference from Frameworks in Other Languages
We can’t generalize the pitfalls of CSS frameworks to other types of frameworks, such as web frameworks like Flask, Rails, or Spring. Developers often override the internals of CSS frameworks but rarely have to do so when using web frameworks. For example, it would be exceptional to need to read through the Flask source code to write a modification that changed how Flask does routing or session management. However, it’s quite common for developers using MUI to use styleOverrides
to modify how to render a slider. The fact that CSS framework code is often overridden is what makes CSS frameworks so dangerous.
Write Your Own CSS
When you write your own CSS, you typically start with a reset, a theme, base CSS styles, and components. I prefer to write these from scratch each time, but many developers find this too time-consuming. To reduce boilerplate, you may want to consider using a CSS starter codebase to provide your base styles. Developers add the starter CSS directly to their codebase rather than adding it as an external dependency. Starters give developers the benefits of a framework (reduced boilerplate, increased quality, and consistency) without the downsides.
I maintain a CSS starter, available for anyone to use, but if that’s not to your liking, you can create your own using one of the following options as a starting point.
- One of your existing codebases (built without a framework, of course)
- An open-source codebase with clean styles
- A minimal CSS framework like Pico CSS
Keep in mind that with any of these options, you’ll likely want to start with just a fraction of their CSS, then copy in new pieces over time. Modify the starter’s styles as your design evolves rather than override them.
Build with CSS
I believe that the best language to use to write application styles is CSS. New CSS features like variables, scopes, nesting, and value functions mean languages like SCSS or JS-to-CSS no longer add enough value to offset their increased complexity. IDE support for CSS is excellent, and support for SCSS or JS-to-CSS often lags behind. Furthermore, developers need a strong understanding of CSS to write and maintain custom styles, regardless of the language in which the styles are written.
Theming, writing scoped CSS, writing expressive CSS, and modifying CSS values are examples of common problems that used to be difficult to solve with pure CSS. These deficiencies in CSS used to push developers away from CSS and toward SCSS and JS. However, newer CSS features have helped to close the gap, reducing the need for other solutions.
Theming
Developers can now add theming to CSS using CSS custom properties (variables). Themes can respond to users’ preferences for dark or light modes using the prefers-color-scheme
media query.
When structuring a theme, declare raw CSS colors as variables at the top of the theme file. Next, declare semantic variables, like --text-color
and --background-color
for a base theme. Finally, override the semantic variables as necessary for any additional themes (like a dark theme). Use the semantic variables as values for all colors in the rest of your code to make sure your application reacts properly to themes.
:root {
--black: #222222;
--white: #FFFFFF;
--text-color: var(--white);
--background-color: var(--black);
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: var(--white);
--background-color: var(--black);
}
}
body {
color: var(--text-color);
background-color: var(--background-color);
}
CSS scopes
CSS scopes make it possible to scope styles to a given element or component. Scopes allow developers to create styles for specific components without worrying about how they impact another area of the codebase (and without needing to define overly specific rules). Browser support is quickly improving for scopes, so expect to be able to use them without restriction soon. The following code colors the h1
red but doesn’t affect any h1
elements outside the given section
tag.
<section>
<style>
@scope {
h1 {
color: red;
}
}
</style>
<h1>Hello world</h1>
</section>
Nested Syntax
CSS supports nested syntax similar to the syntax popularized by SCSS, which makes CSS more convenient to write and can help with readability. Nested styles help express logical groupings of styles and reduce the need to duplicate common selectors across many rules.
hgroup {
margin-bottom: 1rem;
h1 {
font-weight: 700;
}
}
Helper functions
CSS has a growing collection of value functions like calc
and color-mix
that allow developers to perform calculations or processing when defining a CSS value. This helps to reduce the number of necessary CSS variables and lets developers define flexible styles that are easily extended.
button {
--button-size: 1rem;
font-size: var(--button-size);
padding: calc(.5 * var(--button-size)) calc(1.25 * var(--button-size));
background: var(--button-color);
}
button.large {
--button-size: 2rem;
}
button.disabled {
background: color-mix(in srgb, var(--button-color) 65%, transparent);
}
Non-CSS complexity
Non-CSS solutions like SCSS or JS-to-CSS have significant drawbacks, making them a poor choice for application styles. The largest drawback is the compile step. If the styles are compiled to CSS at build time, the developer’s workflow and workstation setup become more complex. If the styles are compiled to CSS at runtime, performance can suffer, and compilation failures can impact users. In either case, browsers run (and debug) styles in CSS, so developers need to understand the generated CSS.
In addition, developers need to consider how compile-to-CSS solutions interact with their existing software. Newer frameworks like NextJS or Remix run client-side code both in the browser and on the server. This means style compilation must be able to run both in the browser and on the server, which could be Node or a worker-like environment like Cloudflare Workers or Deno. Many of the current compile-to-CSS options don’t support these environments well. Furthermore, many popular frameworks like React now take advantage of streaming HTTP responses, which significantly complicate runtime compiled styles.
Use Semantic CSS
Use semantic class names (reusable classes with names based on semantic meaning) to group commonly used styles. Semantic classes express the intent of a group of styles to consumers. Naming is one of the most important things we do as developers to make our software easier to use and adapt. As developers, we should put significant effort into naming our CSS classes, especially when developing a system to be modified and extended by others (after all, software is read more often than it is written).
Semantic class names also allow developers the flexibility to determine their templating strategy. Atomic CSS class names (single-purpose classes with names based on visual function, as popularized by Tailwind CSS) force developers to reduce markup duplication by creating fine-grained UI components or partials. Developers must break out components to encapsulate styles, leading to overly general components with a large list of confusing options.
Use Modern Layout Options
Modern layout options like Flexbox and Grid allow developers to build responsive layouts with clean markup and CSS. This means that we no longer have to bother with outdated 12-column grid layouts, which limit flexibility and clutter markup. A good rule of thumb is to use Flexbox when laying out elements in one dimension and Grid when laying out elements in two dimensions.
How to Structure Custom CSS
To begin, write or pull in the minimal set of styles you need to build the base global styles for your app. This likely includes a CSS reset, styles for a color theme, basic layout, and typography. As you need more complex components like buttons, dropdowns, tables, modals, tooltips, etc., write or add these styles directly to your codebase.
Treat the application styles as a part of your codebase rather than an external dependency. When your application’s styles diverge from what you started with, modify the base styles instead of overriding them. This helps to keep your styles lean and easy to understand.
Prefer Global and Write Scoped Styles as Needed
Global styles are CSS styles that apply to an entire application. Without global styles, it’s difficult to maintain a consistent look and feel. The first styles you write are likely global styles. These are styles that apply to your entire application and are rarely overridden.
When writing new styles, take some time to determine their scope. At first, it is likely that their scope is limited, so they can be written as narrowly scoped styles using classes or @scope
. Over time, common patterns duplicated in scoped styles may be extracted into global styles. Refactor your CSS often!
Write CSS
In conclusion, while CSS frameworks are popular, I believe they’re often a poor choice to style an application. Build with pure CSS on your next project, either by starting from scratch or using my CSS starter or similar as a starting point. You’ll find that you can quickly build your application’s initial styles and maintain them over time.