DEV Community

loading...

Introducing nanostyled: CSS-in-JS without CSS-in-JS

chrisfrank
I work across the full web stack, with an eye toward making people’s lives meaningfully better, not just marginally more convenient.
・6 min read

Nanostyled is a tiny library (< 1 Kb unminified) for building styled React components. It tries to combine the flexible, component-based API of CSS-in-JS libraries with the extremely low overhead of plain CSS:

Low overhead Flexible, component-based API
Plain CSS
CSS-in-JS
nanostyled

Like the CSS-in-JS libraries that inspired it -- 💕 to styled-components -- nanostyled lets you build UI elements with complex default styles, then tweak those styles throughout your app via props:

<Button>A nice-looking button</Button>
<Button color="blue">A nice-looking button that is blue.</Button>

Unlike a CSS-in-JS library, nanostyled doesn't use any CSS-in-JS. Instead, it's designed to accompany a functional CSS framework like Tachyons or Tailwind. Nanostyled makes functional CSS less verbose, and easier to extract into props-controlled components.

Check out nanostyled on npm for installation and usage instructions, or read on for more context.


Functional CSS?

The basic premise of a functional CSS framework is that you can build complex styles by composing tiny CSS utility classes.

A button styled with Tachyons might look like this in markup:

<button class="bg-blue white br2 pa2">Button</button>

That's a button with a blue background, white text, rounded corners (br2), and some padding on all sides (pa2).

holy hell this is the worst thing I've ever seen
-Adam Wathan, author of Tailwind.css

It's true. Functional CSS is ugly, and defies decades-old best practices re: separating content from styling.

On the other hand, styling with functional CSS scales well across large projects, enforces visual consistency, and makes it easy to build new UI elements without writing any new CSS. Adam Wathan, creator of, Tailwind, defends the approach elegantly here.

Nanostyled makes functional CSS easier to abstract into components, without giving up any of its strengths.

Why building flexible components with functional CSS in React is hard

To make working with functional CSS less verbose, you can extract long class strings into self-contained React components:

const Button = ({ className = '', ...rest }) => (
  <button className={`bg-blue white br3 pa2 fw7 ${className}`} {...rest} />
)

The problem, in this case, is that there's no good way to render our <Button> with a different background color. Even though it accepts a className prop, writing <Button className="bg-red" /> won't necessarily render a red button.

Max Stoiber's recent Twitter poll is a good illustration of why:

How well do you know CSS? Given these classes:
.red { color: red; }
.blue { color: blue; }
Which color would these divs be?
<div class="red blue">
<div class="blue red">

The correct answer, which 57% of respondents got wrong, is that both divs would be blue.

You can't know the answer by looking at just the HTML. You need to look at the CSS, because when two conflicting CSS classes have the same specificity, their order in the markup is irrelevant. Which class wins depends on which one is defined last in the stylesheet.

So to build a robust <Button> with functional CSS, we need to be able to

  1. Declare some stock CSS classes that style it
  2. Expose a convenient API for replacing some of the stock classes with alternatives

This second requirement is key for avoiding counterintuitive class collisions like in Max's poll, and it's the thing nanostyled makes easy.

Building flexible components with nanostyled and style props

Nanostyled works by mapping style props onto class names from your functional CSS framework of choice.

Style props can be named whatever you like, and can each hold any number of CSS classes:

A nanostyled Button

import nanostyled from 'nanostyled';
// This example uses CSS classes from Tachyons
import 'tachyons/css/tachyons.css';

// A Button with three style props:
const Button = nanostyled('button', {
  color: 'white',
  bg: 'bg-blue',
  base: 'fw7 br3 pa2 sans-serif f4 bn input-reset'
});

const App = () => (
  <div>
    <Button>Base Button</Button>
    <Button bg="bg-yellow">Yellow Button</Button>
  </div>
);

/* rendering <App /> produces this markup:
<div>
  <button class="white bg-blue fw7 br3 pa2 sans-serif f4 bn input-reset">Base Button</button>
  <button class="white bg-yellow fw7 br3 pa2 sans-serif f4 bn input-reset">Yellow Button</button>
</div>
*/

When a nanostyled(element) renders, it consumes its style props and merges them into an HTML class string, as per above.

It's totally to you which style props to use. The <Button> above has an API that would make it easy to restyle color or background-color via the color and bg props, but hard to change other styles without totally rewriting the base prop.

A more flexible nanostyled Button

By using more style props, we can make a more flexible button:

import nanostyled from 'nanostyled';
import 'tachyons/css/tachyons.css';

const FlexibleButton = nanostyled('button', {
  color: 'white', // white text
  bg: 'bg-blue', // blue background
  weight: 'fw7', // bold font
  radius: 'br3', // round corners
  padding: 'pa2', // some padding
  typeface: 'sans-serif', // sans-serif font
  fontSize: 'f4', // font size #4 in the Tachyons font scale
  base: 'bn input-reset', // remove border and appearance artifacts
});

Rendering a stock <FlexibleButton /> will produce the same markup as its simpler relative. But it's much easier to render alternate styles:

<FlexibleButton
  bg="bg-light-green"
  color="black"
  weight="fw9"
  radius="br4"
>
  Button with a green background, black text, heavier font, and rounder corners
</FlexibleButton>

When you need a variation that you didn't plan for in your style props, you can still use the className prop:

<FlexibleButton className="dim pointer">
  A button that dims on hover and sets the cursor to 'pointer'
</FlexibleButton>

Sharing style props across multiple components

If you're building multi-component UI kits with nanostyled, I recommend sharing at least a few basic style props across all your components. Otherwise it gets hard to remember which components support, say, a color prop, and which ones don't.

I usually start here:

import React from "react";
import ReactDOM from "react-dom";
import nanostyled from "nanostyled";
import "tachyons/css/tachyons.css";

// The keys in this styleProps will determine which style props
// our nanostyled elements will accept:
const styleProps = {
  bg: null,
  color: null,
  margin: null,
  padding: null,
  font: null,
  css: null
};

/* 
Why choose those keys, in particular? For everything except `css`, 
it's because the elements in the UI kit probably will have some default 
bg, color, margin, padding, or font we'll want to be able to easily override via props.

The `css` prop is an exception. I just like being able to use it instead of `className`.
*/

// Box will support all styleProps, but only use them when we explicitly pass values
const Box = nanostyled("div", styleProps);
/*
<Box>Hi!</Box>
renders <div>Hi!</div>

<Box color="red">Hi!</Box>
renders <div class="red">Hi!</div>
*/

// Button will also support all styleProps, and will use some of them by default
const Button = nanostyled("button", {
  ...styleProps,
  bg: "bg-blue",
  color: "white",
  padding: "pa2",
  font: "fw7",
  // I use a 'base' prop to declare essential component styles that I'm unlikely to override
  base: "input-reset br3 dim pointer bn"
});
/*
<Button>Hi!</Button>
renders
<button class="bg-blue white pa2 dim pointer bn input-reset>Hi!</button>
*/

// Heading uses styleProps, plus some extra props for fine-grained control over typography
const Heading = nanostyled("h1", {
  ...styleProps,
  size: "f1",
  weight: "fw7",
  tracking: "tracked-tight",
  leading: "lh-title"
});

// Putting them all together....
const App = () => (
  <Box padding="pa3" font="sans-serif">
    <Heading>Styling with Nanostyled</Heading>
    <Heading tracking={null} tag="h2" size="f3" weight="fw6">
      A brief overview
    </Heading>
    <Heading tag="h3" weight="fw4" size="f5" tracking={null} css="bt pv3 b--light-gray">
      Here are some buttons:
    </Heading>
    <Button>Base Button</Button>
    <Button css="w-100 mv3" padding="pa3" bg="bg-green">
      Wide Green Padded Button
    </Button>
    <Box css="flex">
      <Button css="w-50" margin="mr2" bg="bg-gold">
        50% Wide, Gold
      </Button>
      <Button css="w-50" margin="ml2" bg="bg-red">
        50% wide, Red
      </Button>
    </Box>
  </Box>
);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

This full example is available on CodeSandbox for you to experiment with.

Nanostyled is available on npm, and you can contribute to the library on GitHub.

Discussion (10)

Collapse
equinusocio profile image
Mattia Astorino • Edited

This is called Atomic CSS not functional css and it being used years ago. This is an old approach that doesn’t works anymore with component based and scoped css. The first problem is that you need to map each css property with a specific value to a class selector, so you need to define eg. .pd-8, .
pd-9 and so on. You can’t use shorthands (how do you write padding: 16px 8px 34px 32px?). These are just some of the ACSS cons

Collapse
chrisfrank profile image
chrisfrank Author

Hi Mattia,

Thanks for your comment. You're right that this approach is also called Atomic CSS, and that it has been around for years.

I think you're also right that it was hard to make functional CSS work well with component-based styles.

But that's precisely the point of nanostyled: it makes it easy to use functional CSS in a component-based era.

As for your comment re: padding, I'm not sure I understand. Whether you can use shorthands or not just depends on whether your functional CSS framework has the necessary classes defined.

In tachyons, for example, this would work fine:

<Button padding="pa3">Padding all around</Button>
<Button padding="pv3 ph2">Lots of vertical padding, less horizontal padding></Button>
<Button padding="pl1">Tiny bit of left padding only</Button>
Collapse
equinusocio profile image
Mattia Astorino • Edited

Do not misunderstand me, technically speaking i like your library :) but i think also it "force" two completely different worlds to work together.

Using shorthand properties sometime allow you to write more concise css and save some bytes. Following my example, where all padding values are different, you can't use the padding shorthand (padding: 16px 8px 34px 32px) because you have to separate each value into longhand form, and it will become:

.pt16 {
  padding-top: 16px;
}

.pr8 {
  padding-right: 8px;
}

.pb34 {
  padding-bottom: 34px;
}

.pl32 {
  padding-left: 32px;
}

Tons of classes to write where it could be just:

.Element {
  padding: 16px 8px 34px 32px;
}

This is just an example, think also animations handling. How many classes do you need to write this example with atomic CSS?

.fadeSlow {
  animation: fade 2s infinite cubic-bezier(0.4, 0, 1, 1);
}

.fadeFast {
  animation: fade 300ms 1 cubic-bezier(0.2, 1, 0, 1);
}

It become:

.anim-fade {
  animation-name: fade
}

.anim-2s {
  animation-duration: 2s;
}

.anim-300ms {
  animation-duration: 300ms;
}

.anim-iter-1 {
  animation-iteration-count: 1;
}


.anim-iter-loop {
  animation-iteration-count: infinite;
}

.anim-cubic-1 {
   animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
}

.anim-cubic-2 {
   animation-timing-function: cubic-bezier(0.2, 1, 0, 1);
}

Why i don't like ACSS? Because in 2018, it can be mooore simple:

@keyframes fade {
   from { opacity: var(--startingOpacity, 0), pointer-events: none; }
   to   { opacity: var(--endingOpacity, 1), pointer-events: auto; }
}

/* One animation to handle in-out */
.FadeAnimation {
  --startingOpacity: 0;
  --endingOpacity: 1;
  --duration: 300ms;
  --iterations: 1;
  --easing: linear;
  --fill-mode: both;

  animation: fade var(--duration) var(--itarations) var(--easing);
  animation-fill-mode: var(--fill-mode);
}
Thread Thread
conorcussell profile image
Conor Cussell

I think it's important to remember that writing atomic css does not preclude you from writing your more simple example.

Clearly writing that animation as a bunch of separate atomic classes would be a bad idea, so in a real world app, you would write it exactly as you did. To me always writing functional css is not a hard and fast rule, just a good tool for preventing bloat and enabling consistency.

Collapse
gilesbutler profile image
Giles Butler

This looks awesome Chris, amazing work!! I’m going to try it in the morning on a project I’m working on. I’m a massive fan of Tailwind and I’ve been looking for a good way to integrate it with React. Can’t wait to try this out 👍

Collapse
vviikk profile image
Vikram Ramanujam • Edited

Why am I looking at camelCase when I see kebab-case in the browser debugging tools? How is this different from general React inline styling, except writing a couple of lines of code less?

With styled-components, you write in CSS. Whilst I'm not the advocate of writing in two languages, the sad truth is that you're debugging in CSS and writing in JS/camelCase. Is it because it's easy to override styles from other frameworks?

Collapse
chrisfrank profile image
chrisfrank Author

With inline styles, the keys are literally the names of the CSS properties React understands: listStyle, background, etc. And the values are actual CSS rules.

With what Nanostyled calls "style props", the keys can be named whatever you like, and they're mapping to CSS class names from your functional CSS framework, not to CSS rules.

// inline, with actual CSS rules
<button style={{ background: 'black', color: 'white', borderRadius: '4px' }}>Inline</button>

// nanostyled, with CSS class names from tachyons.css
<Button bg="bg-black" radius="br3" color="white">Nanostyled</Button>
// `bg` and `radius` are not CSS properties
// `bg-black` and `br3` are not CSS rules
// `color` and `white` happen to be valid CSS, but only by chance

You're seeing kebab-case in the browser debugging tools because the actual styling is being applied by whatever CSS framework you're using with nanostyled, which probably uses kebab-case for its class names.

The advantages Nanostyled has over inline styles are:

(1) You get all the power of whatever CSS framework you pair it with — media queries, hover effects, a design system, etc — but with a CSS-in-JS-like API.

(2) It's easier to keep your design consistent. Inline styles make you specify raw CSS properties in your styles, e.g. 12px font and #010101 color. Nanostyled has you use class names from your CSS framework instead, which means dark-gray ends up being the same color everywhere, instead of #010101 sometimes and #020202 other times, which is what tends to happen when an app lives for a long time or has multiple people working on it.

Collapse
vieko profile image
⚡️Vieko Franetovic

Solid Chris – sure, it is not new but, you this is very useful.

Collapse
chrisfrank profile image
chrisfrank Author

Thanks for taking the time to say so, Vieko. Glad it's been useful to you.