Introducing Calico
Performant atomic styling tailored for React developers.
Wall-to-Wall Studios recently published a new open-source styling library named Calico, an atomic styling library tailored for use with React. Since Calico was written with TypeScript, it provides a fully type-safe experience when editing styles.
To get an idea of what using Calico is like, check out the following example:
// theme.ts const theme = createCalicoTheme({ // `breakpoints` allow for the use of mobile-first responsive arrays. breakpoints: { mobile: '0rem', tablet: '48rem', desktop: '75rem', desktopWide: '90rem', }, // `rules` allows you to map any set of values to any CSS property. rules: { color: { primary: '#f0f', }, marginTop: { 0: 0, 1: '1rem', }, }, }) // Example.tsx import * as React from 'react' import { Box } from '@walltowall/calico' const Example = () => { return ( <Box // The styles prop is typed based on your theme! styles={{ color: 'primary', marginTop: [1, 0], }} /> ) }
Why create another styling library?
We’ve decided to develop “yet another styling library” because we wanted to create a library that built on the great ideas of other solutions while mitigating the tradeoffs we’ve experienced while using them in our clients’ sites.
We like atomic CSS
A very popular approach to working with CSS at scale today is to adopt an “atomic CSS” architecture. Atomic CSS refers to the practice of defining a finite-set of classes that represent single-purpose styling units. These single-purpose classes can then be applied and reused across your site or app to make up larger components.
One of the most popular atomic CSS frameworks that Calico drew inspiration from is Tailwind CSS. In our time using Tailwind, we liked its constraint-based styling, intuitive theme configuration, and how it eliminated the need to come up with semantically abstract names for our styles.
What we didn’t like about Tailwind was its opinionated naming conventions and understandable lack of type-safety.
Consider the following example:
<div className="flex relative leading-normal tracking-wide items-center" />
Without knowing Tailwind intimately, it’s unlikely that you’d be able to know
what the classes leading-normal
and tracking-wide
are doing. In addition,
items-center
doesn’t signal if it’s referring to the property align-items
or
the newer justify-items
.
This leads to developers needing to learn a custom CSS API in addition to just knowing CSS. Linters and tooling exists to help with the above, but we found that they weren’t enough to create an experience that felt like writing “just CSS” — without actually writing custom CSS.
We like React-centric APIs
At Wall-to-Wall, we enjoy using React to build our client websites. We like the natural feeling of component props and how intuitive they are when you’re passing “just data” around your app. As such, we believe it is important for our styling solution of choice to feel like a natural extension to React.
Traditional class names in React are awkward
CSS and class names are a natural pattern for the web, but in the React-sphere,
they feel a bit kludgy to use. Dynamic styles with a traditional
className
-based API feel un-intuitive, aren’t conducive to static types, and
require a lot of context switching between your CSS and runtime logic. In
practice, we consider className
strings to be a few levels away from “just
data” in React-land.
// A common approach to dynamic styles with classNames is to use a library like `clsx`. const Card = ({ isActive, isSelected }) => ( <div className={clsx('card', { 'card--active': isActive, 'card--selected': isSelected, })} /> )
There’s nothing above that immediately signals what styles a semantic
className
is applying. This is antithetical to how we like to approach React
at Wall-to-Wall. No longer are we working with data that describes the behavior
of our component, but instead black boxed strings that hook into the CSS world.
React-centric CSS-in-JS is too slow
We used a popular CSS-in-JS library named Styled System for some time before realizing its pitfalls. The prop based API allowed us to express styles in a way that was more in-line with our way of thinking in React:
const Card = ({ isActive, isSelected }) => ( <Box p={[4, 8]} bg={isActive ? 'gray.50' : 'white'} color={isSelected ? 'blue.50' : 'black'} /> )
The React-centric API of Styled System was a breath of fresh-air in comparison to traditional class names and even Tailwind. We no longer had to name everything we style or remember a custom CSS API, and we could easily inspect styles as they were being defined with plain objects and primitives. Styled System also adheres to atomic CSS principles by treating theme based constraints as a first class citizen.
Unfortunately, as we created more complex sites with Styled System, we discovered a consistent problem with sites we built with it. Performance was just not up to our standard.
While we loved the API and developer experience of Styled System (and derivative
libraries like Theme UI), they are fatally flawed without static CSS extraction.
Hooking every single component into a theme and calculating styles at runtime is
“death by a thousand cuts.” You pay a price for every single <Box>
that you
create. This price is especially noticeable during re-hydration (when a
server-rendered React app initially loads) since every single <Box>
is
parsing, computing, diffing and re-inserting their styles all at once. This is a
lot of blocking main thread work.
Styled System performance
The above values are Lighthouse performance metrics for a simulated mobile device (Moto G4) loading one of our larger sites that was built with Styled System. Not good. 13.4 seconds is way too long for a site to become interactive.
CSS prop performance
When we determined that the performance of Styled System was un-viable, we
initially turned to a css
prop based solution built on Emotion and
similar libraries to give us an equivalent but more performant API. Its usage
looked something similar to this:
import { t, mq } from './theme' const Card = ({ isActive, isSelected }: CardProps) => ( <div css={mq({ padding: [t.space[4], t.space[8]], backgroundColor: isActive ? t.colors.gray[50] : t.colors.white, color: isSelected ? t.colors.blue[50] : t.colors.black, })} /> )
With this approach, we retained a similar API to Styled System, but boosted performance by statically importing our design tokens instead of calculating them at runtime. The following scores from Lighthouse were about the average for sites built with this approach:
These scores were significantly better than those produced by Styled System, but we knew we could do better. However, without giving up some of the APIs we’ve grown to like such as responsive arrays, this wasn’t looking to be possible by leveraging libraries like Emotion as is. We needed to look into other performance strategies to do better.
TypeScript support isn’t 100% there
In addition to the issues we outlined above, we found that the TypeScript integration of libraries like Tailwind CSS and Styled System to be less than ideal.
Styled System, despite being React-centric, is difficult to type because of
clashes with existing HTML attributes such as color
. In addition, the very
convenient polymorphic as
prop is very difficult to type correctly and often
lead to dropped styles at runtime in certain scenarios.
// The `as` prop allows us to reuse styling and functionality, but // is very difficult to type properly. const Example = () => { return <Button as={Link}>Example</Button> }
Finally, while not a deal breaker, Styled System is unable to provide type-safety on the actual prop values. This makes it a bit more difficult to enforce consistent usage of theme tokens since there is no type-system or linter to raise a warning when diverging from the theme.
This is a result of the inherent design of the Styled System API which provides the ability to define one-off styles directly at runtime.
// Due to the ability to define one off styles, typos and divergent // styles cannot be warned against. const Example = () => <Box color="primrary" bg="#f0f" />
Tailwind CSS hooks into plain class names, so it is understandably difficult to statically type as a result. Fantastic tooling such as editor extensions do exist to help mitigate this problem, but ultimately cannot reach the same level of safety that a type-checker can due to the inherent nature of CSS classes.
Another issue we ran into with Tailwind and type-safety is the API for defining Tailwind themes. Because of the way Tailwind merges config objects, there currently isn’t a way to have type-safe access to the resolved theme at runtime.
import resolveConfig from 'tailwindcss/resolveConfig' import tailwindConfig from './tailwind.config.js' // `resolveConfig` and `fullConfig` are of type `any` here. const fullConfig = resolveConfig(tailwindConfig)
Enter Calico
Calico is a styling library that aims to combine all of the positives of the previously outlined solutions while mitigating the downsides we expressed.
Statically Extracted & Performant
At its core, Calico is built on top of Treat, a statically extracted CSS-in-JS library. Without going too deep into Treat, you can think of it as a type-safe equivalent of CSS modules. For Calico, Treat is how we generate the set of class names based on your provided design tokens. Treat is also how we primarily achieve full type-safety.
See below for a general example of using Treat, our recommended way to define one-off styles when using Calico.
// example.treat.ts import { style } from 'treat' export const example = style({ color: 'red', }) // Example.tsx import { Box } from '@walltowall/calico' import * as styleRefs from './example.treat.ts' const Example = () => ( <Box styles={{ padding: 4 }} className={styleRefs.example} /> )
With static extraction from Treat, Calico can now provide a React-centric and performant API. On a typical site built with Calico, we typically see scores like the ones shown below:
Much better. It’s worth noting that Calico does technically still have a lightweight runtime — only needing to swap and toggle between pre-existing classes. Much less work than what a more traditional CSS-in-JS library is doing.
Natural theme API
As inspired by Tailwind CSS, Calico generates an expansive set of class names based on a theme object that you provide. Calico provides support for customization and variant support for all valid CSS properties.
Since type-safety is a first class concern for Calico, authoring your theme comes with type-safety and auto-complete for CSS properties.
// theme.ts import { createCalicoTheme, baseCalicoTheme } from '@walltowall/calico' // Define re-used rules with objects. const space = { -1: '-.25rem', 0: 0, 1: '.25rem', } as const // `createCalicoTheme` will merge the provided object with the // minimal `baseCalicoTheme`. const theme = createCalicoTheme({ breakpoints: { mobile: "0rem", tablet: "48rem", desktop: "75rem", desktopWide: "90rem", }, rules: { margin: space, marginTop: space, marginBottom: space, marginLeft: space, marginRight: space, padding: space, paddingTop: space, paddingBottom: space, paddingLeft: space, paddingRight: space, color: { primary: "#f0f", }, // Extend the base theme by spreading the corresponding // rules from the existing theme. width: { ...baseCalicoTheme.rules.width, large: '40rem' } }, // Generate pseudo variants for just the properties you need. variants: { color: { focus: true, hover: true } } })
Polymorphism
Out of the box, Calico supports basic polymorphism support via the component
prop on <Box>
. For type-safe polymorphism for custom components, we’ve opted
to rely on dedicated libraries such as react-polymorphic-box
instead.
This approach reduces the effective scope of what Calico needs to handle and simplifies its implementation.
What more is coming to Calico?
Calico is currently in early release, but is in active development and use by Wall-to-Wall. There are several features we are actively working on to improve the developer experience such as:
- Field aliases as inspired by Tailwind CSS. For example, type-safe access to
marginX
andmarginY
. - Developer friendly runtime errors when accessing invalid theme values.
- A opinionated set of packages such as
calico/contrib
that sets up Calico with more opinionated defaults like those found in Tailwind CSS. - If enough demand exists, we may also look into official packages for common
frameworks such as
gatsby-plugin-calico
andnext-calico
.
For more information on Calico or if you want to give it a try, please take a look at the project on Github!