Skip to content

Introducing Calico

Performant atomic styling tailored for React developers.

Developer
Jun 29, 2020

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

Metrics of Styled System

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:

Metrics of css prop

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:

Metrics of calico

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 and marginY.
  • 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 and next-calico.

For more information on Calico or if you want to give it a try, please take a look at the project on Github!