First, let's quickly compare the differences between making something in React versus a toolchain you might be more familiar with.
React encourages loosely coupled, cohesive components that contain display logic, structure, and presentation. Components describe only themselves, they describe themselves in full, and they know as little as possible about anything outside themselves.
In an MVC framework like Rails, component-like elements (let's say a simple button) are often written in 3 different languages (HTML, Javascript, and CSS) in 3 different places - typically 3 directory trees that are kept in sync with styleguides, labor, and diligent refactoring. Structure, behavior, and presentation are kept separate - typically referred to as separation of concerns. The code in each language is often produced through language-specific transpilers, providing developer niceties but requiring both broad and deep knowledge requirements of languages, transpilers, and application structure to edit a single component.
The React way of thinking is that the above actually achieves separation of technology rather than concerns. Instead, the button's structure, behavior, and presentation should be described in a single place, a single file, even a single language. Vanilla Javascript can be awkward for writing HTML, so Facebook developed JSX for writing HTML-like syntax inside Javascript.
The benefits of writing structure, behavior, and presentation in one language and in one place extend beyond organization. Christopher "vjeaux" Chedeau's deck on CSS in Javascript describes the significant CSS-at-scale challenges solved by moving from monolithic .css files to self-contained components with inline styles written in Javascript.
Quickly run through the CSS in JS deck, as I'll address its points directly when describing Preact's own approach to React CSS later in this article. At a high level, CSS in JS addresses:
Solving these problems in CSS is a huge step forward for building scalable, maintainable UIs at speed. But there are compromises.
Using a single language (Javascript) to write HTML and CSS comes with tradeoffs in syntax and beyond. JSX improves the HTML authoring experience by allowing near-HTML with opening and closing tags instead of using .creatElement
methods.
CSS doesn't look quite so familiar in Javascript - camelCase properties, quoted values, and commas instead of semicolons. This CSS:
.selector {
color: white;
background-color: red;
border-radius: 10%;
}
.modifier {
background-color: yellow;
}
.modifier:hover {
background-color: orange;
}
...becomes something like:
var styles = {
selector: {
color: 'white',
backgroundColor: 'red',
borderRadius: '10%',
},
modifier: {
backgroundColor: 'yellow',
':hover' {
backgroundColor: 'orange'
}
}
};
Close, but a little unfamiliar, subjectively less pleasant, a bit verbose. Not a steep learning curve, but one worth considering if your CSS authors are not yet comfortable in Javascript.
Notice the ':hover'
rule - basic pseudo-selectors like this aren't possible without additional tooling (the above example is written for Radium).
Radium is one of many CSS in JS tools that help develop and manage inline styles on React elements. These tools are required for standard CSS features like pseudo-selectors (eg :hover
) and media queries, which aren't possible in inline styles. Tools like Radium also provide postprocessor features like automated vendor prefixing on inline styles.
I'll note this as a factor that many respond strongly to on first blush, myself included, but is fairly insignificant in React. No one eternally copies and pastes <h1 style='font-size:20px>
like in the bad old days - the style
attribute value is merely the output of a sophisticated CSS tool, and using it sidesteps many inheritance and specificity gotchas endemic to monolithic CSS.
Most sites normalize browser CSS with predictable global defaults. The HTML document rendering your React app will need to load a global .css file at the top of the cascade, to be overridden by component-specific inline styles.
Some combine normalizers with application-specific globals which may require extensive component-specific overrides. For example:
h1 {font-size:3em; border:1px solid black;}
.component h1 {border-bottom:0;}
To avoid this, many set baseline values globally, for components to then extend (rather than reset) as needed:
h1,h2,h3,h4,h5,p… {font-size:1em}
.componentA h1 {font-size:3em; border:1px solid black;}
.componentB h1 {font-size:3em;}
This approach can be further improved with a global dictionary of font sizes, colors, and other values.
Most sites have a color palette that is used across multiple pages and components, a typographic scale (font sizes), and other global values used to build consistent, coherant interfaces. With CSS in JS, this can be solved by defining values in JSON, required by each component that uses them. This is similar to the CSS preprocessor approach of @import'ing global variables.
Similar to a global values, many sites use global utility rules like those in SUIT CSS for quickly applying layout, typography, and theme without writing more lines of CSS. There is some debate over utility class applicability in a component-based system, but I've found them to be quite handy for applying globally-useful classes as inline CSS using component props, for example:
.bgColor-daisy { background-color: #f7cd4f; }
.bgColor-emerald { background-color: #41c487; }
...
<MyComponent bgColor='daisy' />
...
<div style="background-color: #f7cd4f;" />
CSS properties, values, and rules beyond the scope of a single component remains a useful pattern when building in the React way.
Powerful CSS in JS tools like Radium are not designed for globals, reintroducing a use case for CSS pre- and post-processors.
If we're going to write some of our CSS using familiar, mature, sophisticated tooling designed for CSS, is there a way we can write all of it that way while preserving the benefits of CSS in JS as illustrated by vjeaux?
continued our daring tale in Part 2: The Preact Way
At Preact, we use Sass, Webpack, and Postcss + Autoprefixer for writing, compiling, and delivering CSS.
…
@EDIT: brief list of webpack benefits beyond enabling Sass, and link to our webpack config documentation
…
Our components are normal React components that describe structure and behavior, written in JSX. Each component consists of a directory named after the component and its .jsx, for example:
components/
Avatar/
Avatar.jsx
Button/
Button.jsx
If a component needs styles (local or global), we add a Sass file to the component's directory. This improves on the common pattern of duplicating the component directory structure inside a separate stylesheets tree, eg stylesheets/components/button/style.scss
.
components/
Button/
Button.jsx
style.scss
Note: we prefer the Scss syntax for improved readability of multi-line Sass maps and similar features, but will refer to it as Sass (the tool's name) throughout this article for consistency, excepting specific file extensions.
Button.jsx explicitly imports all its dependencies, including the optional style.scss:
import React from 'react';
import Router from 'react-router';
import cx from 'util/StyleClasses.js';
import style from './style.scss';
The component's top-level DOM element is given a class of .this
:
<div className={ style.this } />
The class .this
is compiled by Webpack to a unique string, rewriting both the rendered component's class attribute and the compiled CSS selector. In development the unique class includes the component path and developer-readable class name:
.app-components-Button-style___this---b2q4X { color:red }
This class is minified by Webpack for production deploys:
.b2q4x { color:red }
Additional classes can be derived from props or state by using the Classnames Javascript utility. For example, in Button/Button.jsx:
var classes = {
isActive: (this.state.isActive ? 'isActive' : 'notActive'),
isPrimary: (this.props.isPrimary ? 'isPrimary' : 'notPrimary')
};
var classnames = cx(style, [
'this',
classes.isActive,
classes.isPrimary
]);
<button className={ classnames } />
And in Button/style.scss:
.this {
background: blue;
}
.isActive {
background: orange;
}
.isPrimary {
background: red;
font-weight: bold;
}
Classes on the rendered component are updated in response to state and prop changes in the normal React one-way data flow.
This gets really powerful when we start intelligently sharing Sass globals with individual components. Just as our Button.jsx imports its dependencies, style.scss @imports its Sass dependencies:
@import "./vars/themes/set-global-theme";
@import "./vars/typography";
.this {
background: theme("actions", "branded");
}
.isActive {
background: theme("actions", "branded", "active");
}
.isPrimary {
background: theme("actions", "primary");
font-weight: typography("actions", "primary", "fontWeight");
}
The property values above are the result of some functions we use for easily accessing deeply-nested values in Sass maps. You could just as easily take a global variable approach with a naming scheme like BEM, for example:
.isActive {
background: $theme-actions__branded--active;
}
Defining a large global value set like this speeds the development and design of visually consistent interfaces in the future, much as a comprehensive component library eases functionality improvements. The Sass library as a whole, or single-concern .scss files, can be moved when required to their own repos for versioned provisioning to multiple apps or components. Rather than thinking of them as global variables, consider each as its own reusable component.
Whether one chooses to write Sass, Less, Stylus, or straight CSS, this approach benefits from the feature set of writing real CSS using a sophisticated CSS-specific toolkit of choice. Many best CSS practices still apply (such as limiting specificity), and are further improved by a reduction in scope - the component classes are compiled by Webpack to short uniques (eg .b2q4x
).
Most component Sass files have only a few @imports, but some components intended for repeated use throughout the application and benefit from having a lot of values available.
We use our <Cell>
component as a general-purpose wrapper for applying flexbox, theme, typography, and spacing - often nesting one Cell
inside another for complex layouts. Our Sass library is exposed through a defined set of props on the <Cell>
component, making it easy to develop interfaces without writing any new Sass.
Cell/Cell.jsx:
var Cell = React.createClass({
propTypes: {
bgColor: React.PropTypes.React.PropTypes.string,
…
},
render() {
var classes = {
bgColor: (this.props.bgColor ? 'bgColor-' + this.props.bgColor : ''),
…
};
var classnames = cx(style, [
'this',
classes.bgColor,
…
]);
}
});
Cell/style.scss:
@import "../../stylesheets/classes/color";
UserHeader.jsx:
import Avatar from 'components/Avatar';
import Cell from 'components/Cell';
<Cell bgColor='app-background' color='app-color' paddingH paddingV>
<Cell bgColor='user-background' flex='row' typographyEnabled paddingH='small' paddingV='small'>
<Avatar size='100'>
<h1>Jane Doe</h1>
</Cell>
</Cell>
Using just a single utility component's props, we can will apply a global theme and simple layout to a user's avatar and name.
In general, Sass is still the way to go. Be kind to future you: maintain a consistent authoring environment, with minimal and predictable places to look for style rules.
We have found a repeated exception, however. Our <Cell>
component is a general purpose wrapper for flexbox, theme, typography, and spacing controlled through props. Rather than duplicating the flexbox api with props for each flexbox property that are piped to single-purpose CSS rules, it's much more convenient to configure the flexbox properties through inline styles. For example:
<Cell flex='row' style={{ flexWrap: "nowrap", justifyContent: "flexStart" }}>
This pattern comes up any time it's useful to style a component directly without props. In the case of flexbox, this raises a valuable use case for using a CSS in JS library like Radium for automating vendor prefixes on inline styles.
Automated prefixing dynamic inline auto prefixing is more complicated than static server side autoprefixing - will it need to run as the client browses, in the application bundle? Prefix only for detected user agent? Will it impact perceived performance? Solutions vary, but our provisional recommendation is to use Radium for arbitrary inline definitions (used sparingly), and let Radium sort out vendor prefixes.
One unique capability of CSS written in JS is shared constants with the display logic that determines how a component renders. For example, the number of buttons dynamically rendered in a toolbar could affect the padding between each button with some simple math written in Javascript.
In my experience, this is often (not always) a trap on the way to truly composable layouts, which can be more concisely and reasonably described by designing for fluid, flexible, unpredictable content from the start. That said, writing styles in Sass does not preclude dynamic values in the style
attribute - inline styles will simply override CSS provided elsewhere as per the normal cascade.
Webpack + Sass + Postcss addresses the limitations of CSS in JS inline styles, even when written with a tool like Radium. It gives CSS developers a familiar authoring environment (Sass), with sophisticated language-specific tooling, that can be applied at a component and global level. And it doesn't prevent writing the occasional dynamic Javascript value into a component's style
attribute for when sharing constants between Sass and Javascript is required.
In Part 3, we'll see how it stacks up against the 7 scalable CSS challenges solved by CSS in JS listed by Vjeaux.
And now the thrilling conclusion… Head to Head: CSS in JS vs Webpack + Sass
In Part 1, we covered the new high water mark in scalable CSS when written in Javascript, as well as some of the pitfalls.
In Part 2, we covered how Preact uses Webpack + Sass + Postcss to achieve many of the same goals in a development environment that's more comfortable for CSS.
In Part 3, we'll see how Webpack + Sass delivers on the CSS in JS scalable CSS benefits point by point.
But first, a spoiler. Both approaches check the same boxes, with most of this boiling down to implementation details. There is, however, one exception.
A reoccurring problem in Webpack + Sass implementation is classless element selectors (eg h1{}
, instead of h1.uniqueClass{}
). Without a class, there's nothing for Webpack to compile into a unique, shoot-to-kill selector string. Without a unique selector, classless element selectors affect the DOM outside of the component they were intended for. They work (almost) like they always have - affecting every matched element globally, colliding with other classless element selectors, with final render according to specificity and source order.
Source order is where React can make things worse than usual. If component CSS is lazy loaded (ie, only loaded when the component requiring it is rendered), source order of CSS becomes unpredictable.
For example, Bob loads ComponentA, then ComponentB. Both component's have classless element selectors for H1 elements:
Component A:
h1 { color: red; }
Component B:
h1 { color: blue; }
Because Bob loaded ComponentB last, it's styles were loaded later and come later in the source order. Any H1 elements that had been red are now blue, and they'll continue to be blue until another component is loaded that overrides ComponentB's H1.
Meanwhile, Mary loaded ComponentB, then ComponentA. All of Mary's H1's are red. Have fun trying to debug issues like this!
Fortunately, the fix is easy - don't write classless element selectors! And, if you're writing element + class selectors, you should probably adhere to general CSS best practices and just use a class, omitting the element entirely. Problem solved.
As all rules have exceptions, this rule has one - element selectors intended for global effect, such as browser normalizing, can be safely used if included in the HTML template before any component styles are loaded.
Feel free to follow along with the CSS in JS deck. For fairness, I'm assuming the CSS in JS is written using a tool like Radium to provide CSS-like pseudo-selectors, media queries, and automated vendor prefixing.
Winner: Webpack + Sass - useful globals preserved, problems easily sidestepped.
Solved by generating component-specific class names (eg .button---N1KXM
in dev, .N1KXM
in prod). Preventing name collisions preserves component isolation.
Bonus: we can use the same CSS authoring tools (Sass) for normalizing browser defaults with the application's theme, typographic scale, etc.
Not Solved: classless element selectors defined inside components (eg h1{}
) still have global scope, as described at the top of this article (Part 3). Avoid writing classless element selectors. CSS in JS does not have an equivalent problem, as all styles are applied inline instead of through selectors.
Bonus: Define global utility classes like those in SUIT CSS with Sass. We primarily use utility classes controlled through props, like .bgColor-action-primary
, as described in Part 2.
Winner: draw.
Solved by requiring @import of helper, var, and class Sass in each component's Sass. Failing to @import required dependencies (mixins, functions, variables...) noisily prevents compile, with specific error messages printed in the terminal.
To Be Improved: we need to write noisy errors and warnings for SassScript function returns that do not break compilation. For example, using a function to return the value for a Sass map key that does not exist will return null
, which is silently omitted from compilation rather than producing a warning.
Winner: draw - neither approach automatically removes unused style rules, both reduce manual search area scope to individual components.
Solved: classnames in JSX that do not have a corresponding CSS rule will not be printed to the DOM in the element's class
attribute.
Not Solved: going the other way, rules in CSS will be compiled even if no JSX classes exist that can use them.
Winner: draw.
Solved by Webpack configuration. Class names are shortened to short, unique strings. Comments and whitespace can be stripped by Webpack or Postcss. Compiled CSS redundancies can be further removed through investments in Postcss.
Winner: draw.
Not Solved: sharing constants between CSS and JS is still terrible. In Ye Olden Dayes, developers would write comments everywhere concerned (CSS, JS, HTML…) to remind their future selves to update when making changes. But even with the best of intentions, file names, locations, and line numbers constantly change independently of direct edits to commented constants, rapidly deprecating their usefulness.
Another Ye Olden Solushen is to define constants in a third place (PHP, Rails...), which is then read by Sass and JS. This gets a little ridiculous.
However: using Webpack + Sass does not prevent applying Javascript dynamic values directly to the element's inline styles in JSX, just as one would without Webpack + Sass. In general, this should be avoided - aside from writing styles in more than one place, shared constants are often an indicator to simplify.
Winner: draw.
Solved for component CSS rules, as all rules are scoped to their parent component with unique selectors.
Not solved for classless element selectors when CSS is lazy loaded, as noted above. Don't write classless element selectors.
Winner: CSS in JS.
Not Solved: classless element selectors can be abused to break isolation and style child elements. For example:
JSX:
<div className={ style.this }>
<Cell>
<Avatar />
</Cell>
</div>
CSS:
.this div { background: red }
All <div>
elements inside the parent .this
will be given a red background, including any div
's in Cell
and Avatar
. Of course, this is solved following the same Golden Rule established throughout this article: avoid classless element selectors.
Solved for component-scoped classes. Classes are generated with unique names (eg .N1KXM
) during build, and cannot be referenced outside of their own component Sass.
We think all of the CSS at scale challenges above are satisfactorily addressed by Webpack + Sass (provided you yada yada classless yada element selectors), with the added benefit of using a mature, language-specific tool for writing CSS.
We did find two cases where writing CSS in JS remains valuable - sharing constants, and automated vendor prefixing of inline styles. Fortunately, we can still use a tool like Radium to help with the first and automate the second.