Skip to content

Latest commit

 

History

History
263 lines (195 loc) · 7.6 KB

ATOMIC_CSS.md

File metadata and controls

263 lines (195 loc) · 7.6 KB

Atomic CSS

What is Atomic CSS?

Atomic CSS is an approach to styling that reduces payload sizes for style delivery, and allows style composition and reuse easily. This document describes the concept of Atomic CSS, its advantage and use cases.

Atomic CSS is a way of writing CSS such that each CSS rule has exactly one declaration (an "atom"). For example,

/** A normal class */
.myClass {
  background: red;
  width: 100%;
  height: 100%;
}

/** Can be written atomically as: */
.a {
  background: red;
}
.b {
  width: 100%;
}
.c {
  height: 100%;
}

Usage in Linaria

Atomic styles can be enabled in the linaria config by providing an atomizer function (see configuration for details).

Once enabled, it is possible to write atomic styles by importing the css template literal from @linaria/atomic:

import { cx } from '@linaria/core';
import { css } from '@linaria/atomic';

const atomicCss = css`
  background: red;
  width: 100%;
  height: 100%;
  border: 1px solid black;
`;

const blueBackground = css`
  background: blue;
  border: 1px solid black;
`;

// In React:
<div className={cx(atomicCss, blueBackground)} />;

// In vanilla JS:
const div = document.createElement('div');
div.setAttribute('class', cx(atomicCss, blueBackground));
document.body.appendChild(div);

Which at build time, is transformed into:

import { cx } from '@linaria/core';
import { css } from '@linaria/atomic';

const atomicCss =
  'atm_background_abcd atm_width_efgh atm_height_ijkl atm_border_mnop';

const blueBackground = 'atm_background_qrst atm_border_mnop';

// In React:
<div className={cx(atomicCss, blueBackground)} />; // <div class="atm_width_efgh atm_height_ijkl atm_border_mnop" atm_background_qrst />

// In vanilla JS:
const div = document.createElement('div');
div.setAttribute('class', cx(atomicCss, blueBackground)); // same as React example
document.body.appendChild(div);

(Note: in the example above, the slugs in the atoms are lengthened for readability)

The format of these atoms is atm_${propertySlug}_${valueSlug} which lets us deduplicate based on the propertySlug part of the atom.

As you can see in the above example, atm_border_mnop can be removed as it duplicated, and we see two atoms with the background property slug, and can remove one of them.

at-rules, pseudo classes and keyframes

Linaria's atomic css also supports creating (nested) at-rules, pseudo classes and keyframes:

import { css } from '@linaria/atomic';

// Note: animation name namespacing does not happen automatically with @linaria/atomic. Keyframe animations are pulled out to the top level and not atomized.
export const animation = css`
  @keyframes my-animation {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  animation: my-animation 1s infinite;
`;

export const pseudoClass = css`
  &:hover {
    color: pink;
  }
`;

export const mediaQuery = css`
  @media (max-width: 400px) {
    background: orange;
  }
`;

These can also be combined for further nesting.

Property priorities

Using atomic CSS, longhand properties such as padding-top have a higher priority than their shorthand equivalents like padding. For example:

import { css } from '@linaria/atomic';

const noPadding = css`
  padding: 0;
`;

const paddingTop = css`
  padding-top: 5px:
`;

// In react:
<div className={cx(noPadding, paddingTop)}>...</div>;

The result will be that the div has padding-top: 5px;, as that is higher priority than padding: 0.

The way linaria achieves this is through property priorities. See this blog post for more details on the concept, and the problems it solves. The method used in linaria is to increase the specificity of the rules: see @linaria/atomic's propertyPriority function for a list of longhand and shorthand properties supported by this. The basic rules are:

  • Longhand properties have higher priority than shorthand properties
  • Declarations in @media rules (and any @-rule, such as @supports) have higher priority than those outside of them

Use cases

Reducing number of rules

One advantage of writing styles in this way is that we can reuse CSS more effectively. In many cases, declarations are repeated in CSS, and atomic CSS allows heavy reuse of these classes. For example if we have the classes,

<style>
  .redButton {
    background: red;
    width: 100px;
  }

  .blueButton {
    background: blue;
    width: 100px;
  }
</style>
<button class="redButton" />
<button class="blueButton" />

We've repeated the declaration width: 100px twice. This repetition can become heavy as the amount of rules increases. Atomically, this can be written as:

<style>
  .a {
    background: red;
  }
  .b {
    background: blue;
  }
  .c {
    width: 100px;
  }
</style>
<button class="a c" />
<button class="b c" />

Application order precedence vs. Definition order precedence

Another advantage is that rule precedence can be managed upon application to an element rather than in the declaration. Consider a React component that passes a class name through a prop:

// BaseComponent.js
import { cx, css } from '@linaria/core';
const myClass = css`
  background: red;
`;

export default function BaseComponent({ overrideClass }) {
  return <div className={cx(myClass, overrideClass)} />;
}

// OverrideComponent.js
import { cx, css } from '@linaria/core';
import BaseComponent from './BaseComponent';

const overrides = css`
  background: blue;
`;

function OverrideComponent({ overrideClass }) {
  return <BaseComponent overrideClass={overrideClass} />;
}

The expectation would be that background: blue should be the result for OverrideComponent, but the result depends on the ordering of the compiled output in the DOM:

// dist/BaseComponent.js
import { cx, css } from '@linaria/core';
const myClass = 'myClass';

export default function BaseComponent({ overrideClass }) {
  return <div className={cx(myClass, overrideClass)} />;
}

// dist/BaseComponent.css
.myClass {
    background: red;
}

// dist/OverrideComponent.js
import { cx, css } from '@linaria/core';
import BaseComponent from './BaseComponent';

const overrides = 'overrides';

function OverrideComponent({ overrideClass }) {
  return <BaseComponent overrideClass={overrideClass} />;
}

// dist/BaseComponent.css
.overrides {
    background: blue;
}

Since .overrides and .myClass have the same specificity, the ultimate result of which component has precedence depends on their ordering in the DOM. This may be stable, but for async components, it may produce race conditions in styling.

With atomic styles, the precedence can be managed upon application – cx has the ability to look at the classes it is provided with, and filter duplicates. This means that there should never be rules with the same precedence applied to the same element.

Note that this example uses React, but is not a unique problem to react – it applies to any files that deliver bundles split between different files, where the ordering (and ultimate winner in specificity) may be nondeterministic at runtime.

Limitations

  • @linaria/atomic does not support targeting child selectors in css
  • Shorthand properties like background are discouraged, as conflicts may still occur between properties like border and border-top
  • Usage of cx is required to correctly deduplicate atoms, to ensure the application order precedence is guaranteed.

See also