A Million Little Pieces: Functional CSS in React
There have been a great many thoughts by thought-leaders thinking thoughts about the future of CSS in a JavaScript encapsulated world. A lot of those thoughts seem to portend the death of CSS as a wholly separate language to describe presentational relationships of various pieces of markup. And, to a large degree, many of those thought-thinkers are right. CSS and its importance in the new web ecosystem has evolved a lot in the past few years and not necessarily in a way that has been kind to the new frameworks on the block nor to the idea of an inheritance-based presentational layout language. Playing nice with React and CSS hasn’t always been F-U-N, but using JS and CSS together can at least be F-U-N[CTIONAL] and maybe a little bit easier.
Thinking Thoughts on Functional CSS
The purpose of this essay isn’t to convince you to adopt a functional CSS system, though I highly suggest you do. BassCSS is a good one. Tachyons is another good one if you enjoy pain. Tailwind is pretty fresh and new. Solid is a nice one, but I have to say that because I’m a contributor. There are a great many articles extolling the virtues of implementing a functional CSS system, so I won’t say much more about it here.
Okay, I lied, I will say a little more about functional CSS. Functional CSS is great because it prevents people from writing more CSS. And let me present the following argument:
The best CSS you write is the CSS you don’t write.
The functional approach enforces and maintains consistency across your product by paring things like type-scale, margins, paddings, and other things that you don’t want to be variable down to a very small set of options. At the same time, functional CSS keeps your dependencies down to a compact and performant singular CSS import. Further, you encounter fewer issues with regard to inheritance, specificity, and the collisions that result from CSS because each of your functional classes has one, and only one, CSS rule attached to it.
All this isn’t to say that functional CSS is infallible. Rather, the purpose of this exercise is to explore a way we can lessen the pain of implementing a functional CSS system when used in a React environment. These pain points are as summarized in bold as follows:
Functional CSS relies on a large number of idiosyncratic class names whose documentation lives somewhere else (if at all). The correct application of which becomes esoteric knowledge.
{% raw %}
// Via Tachyons
<article class="center mw5 mw6-ns hidden ba mv4">
<h1 class="f4 bg-near-black white mv0 pv2 ph3">Title of card</h1>
<div class="pa3 bt">
<p class="f6 f5-ns lh-copy measure mv0">
....
{% endraw %}
Elements with functional CSS classes grow to become ultimately unreadable. This adversely affects code clarity and the developer experience.
{% raw %}
// Also via Tachyons
<a class="no-underline near-white bg-animate bg-near-black hover-bg-gray inline-flex items-center ma2 tc br2 pa2" href="https://github.com/" title="GitHub">
{% endraw %}
There is no accountability in writing class names. Developers can, and will, apply completely contradictory functional classes to elements with unpredictable results.
{% raw %}
// This is all valid to write, but who knows what will render!
<div className="mg0 mg1 mg2 flex flex-row flex-column block inline float-left float-right hide show">
...
{% endraw %}
None of this is very React-y and the more you think in React, the stranger it will all feel. That’s because developers and designers who build in React, end up thinking in React and having to switch contexts to CSS and class names from a JS world will generate some cognitive friction.
This strange feeling has lead to a bunch of different ways to think about CSS in JavaScript and the evolution of solutions like Radium, Styled Components, Glamorous and many more. All of these abstractions bring CSS closer to the components they describe (a good thing™!), and allow users to define UI via CSS properties (also a good thing™!). However, none of these solutions give you the consistency, portability, and performance of functional CSS.
Further, even though it’s not vogue right now, there is a lot of benefit to keeping your source of truth for UI presentation in a segregated Sass/SCSS/CSS environment due the fact that it’s highly portable. CSS defined in a modular (BEM/OOCSS) and/or functional way will also be able to serve legacy and other non-react web platforms well into the foreseeable future.
Finally, maybe you’re like Shopify or one of the many other organizations building React UI component libraries. In which case, all UI component configuration is done via properties and CSS and class names are even further removed from the common workflow. At that point, making developers and designers implement simple layout and text decoration via CSS is just cruel.
Representing Functional CSS in React
You don’t have to throw away a perfectly good, exhaustive, and performant functional CSS toolset to make it in a React-forward world. A lot of these functional principles actually translate even better into a modular, JavaScript environment.
We are no longer going to apply classnames to elements, nor write CSS as style strings in React components, instead the concern of the developer will be configuring components that represent proxies to our well-defined functional system that is either included as a root import dependency in your sass-loader or imported directly by any of the components that utilize it. This approach can apply to any available functional library or one that you define. We can thank our Senior Front-end Developer James Panter for pioneering this approach for us here at Twitch.
These proxy components exist as metaphors described by:
Layout
— A component that concerns itself primarily with properties of spacing, alignment, display, and position.Styled
Layout`` — A component that inherits the properties of Layout and also can apply typographic and visual styles like font-size, color, background-color, emphasis, etc.Text
— A component that concerns itself with the display of text styles, but can also render as any chose element type (span, p, h1-h6) where the other components necessarily render as divs.InjectLayout
— A utility that doesn’t render an element but rather injects class names into any arbitrary element or component that has a className property.
These are the metaphors that are working well for our team, but any set of component metaphors can be adopted as long as their configuration properties can proxy to existing functional classnames.
What This Means in Practice
All functional classes are scoped to 3 components. StyledLayout
, Layout/InjectLayout
, and Text
can be used to compose almost all feature UI in concert with common components like Button
, Tab
, etc.
The only remaining CSS is used for widths/heights**.** This is a bit of a an exaggeration. There are also rules that define custom animations, and rules that have to exist so that modifier-based inheritance can be respected on state-change. That said, there is nothing stopping you from allowing users to directly set width/height on Layout
level elements, a path we’ve not yet pursued.
{% raw %}
// For example:
<StyledLayout display={Display.InlineFlex} alignItems={AlignItems.Center} padding={2} border>
<Text color={Color.Alt}>I'm a component composed entirely using functional CSS!</Text>
</StyledLayout>
{% endraw %}
CSS rules are exposed by a well-defined, documented API that can be accessed via intellisense/autocomplete. The implementation of these rules via configuration properties is enforced by TypeScript and keeps developers accountable while also holding their hand.
{% raw %}
// text/component.tsx
// Font sizes are exposed on the Text component via an enum
export enum FontSize {
Size1 = 1,
Size2,
Size3,
Size4,
Size5,
Size6,
Size7,
Size8,
}
{% endraw %}
{% raw %}
// ...
// The are mapped to corresponding functional classes.
const TEXT_FONT_SIZE_CLASSES = {
[FontSize.Size1]: 'font-size-1',
[FontSize.Size2]: 'font-size-2',
[FontSize.Size3]: 'font-size-3',
[FontSize.Size4]: 'font-size-4',
[FontSize.Size5]: 'font-size-5',
[FontSize.Size6]: 'font-size-6',
[FontSize.Size7]: 'font-size-7',
[FontSize.Size8]: 'font-size-8',
};
{% endraw %}
{% raw %}
// ...
// And applied within the component
if (props.fontSize) {
classes[TEXT_FONT_SIZE_CLASSES[props.fontSize]] = true;
}
{% endraw %}
{% raw %}
// ...
// my-feature/component.tsx
// The functional properties are then invoked in the feature component.
{% endraw %}
{% raw %}
import { Text, FontSize } from 'text/component';
{% endraw %}
{% raw %}
<Text fontSize={FontSize.Size4}>Hello World</Text>
{% endraw %}
Standardized APIs allow for complex composition of responsive rules:
{% raw %}
`<Layout
display={Display.Flex}
flexGrow={0}
flexWrap={FlexWrap.NoWrap}
margin={{ bottom: 2 }}
breakpointExtraSmall={{
alignSelf: AlignSelf.End,
flexOrder: 1,
}}
breakpointLarge={{
alignSelf: AlignSelf.Start,
flexOrder: 2,
}}
/>`
{% endraw %}
The source of truth remains in the functional system. Your key tools for composing UI remain portable to other platforms and consistent across your application ecosystem.
You write more complex, composed UI in React while your CSS stays the same size. It works great at scale since adding more UI doesn’t increase the amount of CSS you add to the system.
CSS in JS is not a revolution, it is an evolution. Using functional CSS libraries in React may not seem obvious at first, but it has proven to be a tremendously useful approach that has allowed our teams to remain quick and consistent while building our products from millions of little pieces.