Concepts

CSS in JS is not just about throwing some CSS into .js files. CSS in JS comes with a different approach. You can still use most of the CSS constructs you are familiar with but there are some quirks and limitations when a different approach is required. Those restrictions are not necessarily bad but it can be certainly frustrating if you are not aware of them. This page aims to answer most of the "How to do x in Styletron" questions and suggest some good practices.

Most of the concepts described bellow are not React specific but we still use React and styletron-react to demonstrate them.

  1. Style Object
  2. Style Function
  3. Media Queries And Pseudo Classes
  4. Selectors
  5. Fonts
  6. Keyframes
  7. Vendor Prefixes
  8. Pitfalls

Style Object

CSS properties are defined as a JS object and that has some limitations compared to the pure CSS:

.btn {
  color: red;
  font-size: 16px;
  padding-left: 1em;
  padding-right: 1em;
}

translates into

editable
import { styled } from "styletron-react";

export default () => {
  const Button = styled("button", {
    color: "red",
    fontSize: "16px",
    paddingLeft: "1em",
    paddingRight: "1em"
  });
  return <Button>Button</Button>;
};

We use camelCase for property names since JS can't handle - in the object key. Although, you could add quotes to work around it:

{
  "font-size": "16px",
  "padding-left": "1em",
  "padding-right": "1em"
}

This code still works but we usually try to avoid quoting object keys in JavaScript.

Style Function

props => CSS styles

Component styles often change based on props. We call it dynamic styling. styled's second argument can accept a function that gets props and returns a style object:

editable
import { styled } from "styletron-react";

export default () => {
  const Button = styled("button", props => ({
    fontSize: props.$compact ? "12px" : "16px",
    padding: props.$compact ? "0.25em" : "0.5em",
    margin: "0.5em"
  }));
  return (
    <>
      <Button>Standard Button</Button>
      <Button $compact>Compact Button</Button>
    </>
  );
};

If you wonder why we called our prop $compact and not just compact, see the explanation.

Media Queries And Pseudo Classes

The style object can be nested so you can define media queries and most of pseudo classes:

editable
import { styled } from "styletron-react";

export default () => {
  const Block = styled("div", {
    backgroundColor: "#DDD",
    color: "#000",
    padding: "0.5em 1em",
    border: "2px solid black",
    ":hover": {
      border: "2px dashed darkred"
    },
    "@media screen and (max-width: 880px)": {
      backgroundColor: "#276EF1",
      color: "#fff"
    }
  });
  return <Block>I get blue when the browser window shrinks</Block>;
};
I get blue when the browser window shrinks

It might be a good idea to use the same breakpoints across multiple components. You can just abstract them into a constant:

const MOBILE = "@media screen and (max-width: 880px)";
const styles = {
  [MOBILE]: {
    backgroundColor: "#276EF1",
    color: "#fff"
  }
};

There is a small caveat regarding the order of media queries.

Selectors

One of the primary benefits of CSS in JS is that it keeps styles encapsulated with markup and avoids specificity concerns. It is probably the most controversial thing about CSS in JS since it requires an alternative approach from other patterns.

Styletron doesn't support simple selectors and combinators. You can still use pseudo-classes and pseudo-elements.

CSS is a powerful language and many complex selectors and combinators just don't fit into CSS in JS model well. There are definitely valid cases when things like descedant combinators can be used, and we have been exploring a possible API for that.

For now, there are some workarounds for common cases that we recommend to use. However, if you are trying to implement something like this:

div:nth-of-type(2) ul:last-child li:nth-of-type(even) * {
  font-weight: bold;
}

Then you're probably doing it wrong and should move your styles closer to the styled elements.

Child Combinator

CSS syntax: A > B

Any element matching B that is a direct child of an element matching A.

For this use-case, you can use React.Children.map and React.cloneElement to pass additional children props. A very useful prop might be the child index so you can simulate :nth-child CSS pseudo class:

editable
export default () => {
  const ButtonGroup = ({ children }) => {
    return React.Children.map(children, (child, index) =>
      React.cloneElement(child, { groupIndex: index })
    );
  };

  const Button = ({ groupIndex, children }) => {
    const Btn = styled("button", props => ({
      margin: props.$isGrouped ? "0px" : "0 2em 0 0"
    }));
    return (
      <Btn $isGrouped={typeof groupIndex !== "undefined"}>
        {children} {groupIndex}
      </Btn>
    );
  };

  return (
    <>
      <p>
        <ButtonGroup>
          <Button>One</Button>
          <Button>Two</Button>
          <Button>Three</Button>
        </ButtonGroup>
      </p>
      <p>
        <Button>One</Button>
        <Button>Two</Button>
        <Button>Three</Button>
      </p>
    </>
  );
};

This works only if Button is a direct child of ButtonGroup. One downside of this solution is prop name pollution. The prop groupIndex is hard-coded in ButtonGroup and Button component can't use it for other purposes. However, it's usually not a big problem in real applications. Widely used higher-order components do the exactly same thing.

On the other hand, you can now test all the visual Button states even without ButtonGroup. You can set the groupIndex explicitly

<Button groupIndex={0}>One</Button>
<Button groupIndex={1}>Two</Button>
<Button groupIndex={2}>Three</Button>

to simulate the ButtonGroup's functionality.

Descendant Combinator

CSS syntax: A B

Any element matching B that is a descendant of an element matching A (that is, a child, or a child of a child, etc.). the combinator is one or more spaces or dual greater than signs.

If you want to control styles of descendant components, you can use React Context and the context hook.

editable
export default () => {
  // createContext should be module scoped
  // and imported by Parent and Child
  const ParentContext = React.createContext();

  const Parent = ({ children }) => {
    const { Provider } = ParentContext;
    return <Provider value="blue">{children}</Provider>;
  };

  const Child = () => {
    const Text = styled("div", props => ({
      color: props.$color
    }));
    const value = React.useContext(ParentContext) || "black";
    return <Text $color={value}>This text is {value}.</Text>;
  };

  return (
    <>
      <Parent>
        <div>
          <Child />
        </div>
      </Parent>
      <Child />
    </>
  );
};
This text is blue.
This text is black.

Child is not a direct descendant of Parent and yet, it still gets the value blue. We are crossing component boundaries without passing additional props or using a CSS combinator.

Descendant Hover

React hook useHover provides a nice concise API to have full control over hover states:

editable
export default () => {
  const [hoverRef, hovered] = useHover();
  const Parent = styled("div", {
    backgroundColor: "#DDD",
    textAlign: "center",
    padding: "0.5em"
  });
  const Child = styled("span", props => ({
    fontSize: "2em",
    borderBottomWidth: "2px",
    borderBottomStyle: "solid",
    borderBottomColor: props.$hovered ? "darkred" : "#DDD"
  }));
  return (
    <Parent $ref={hoverRef}>
      <Child $hovered={hovered}>{hovered ? "😁" : "☹️"}</Child>
    </Parent>
  );
};
☹️

This solution avoids using CSS altogether and lets you pass hovered anywhere you want.

Since JS is used instead, it's less efficient but preserves the component encapsulation.

Fonts

Do you need to use @font-face? Styletron provides a declarative way to do it. You can turn

@font-face {
  font-family: "Laila";
  src: url("/static/Laila.ttf");
}

.font-laila {
  font-family: "Laila", serif;
}

into

editable
import { styled } from "styletron-react";

export default () => {
  const FontLaila = styled("div", {
    fontFamily: {
      src: "url(/static/Laila.ttf)"
    }
  });
  return <FontLaila>This is Laila font.</FontLaila>;
};
This is Laila font.

Keyframes

CSS offers a powerful animation API @keyframes. Styletron provides a declarative way to use it. You can turn

@keyframes move {
  from {
    margin-left: "150px";
  }
  to {
    margin-left: "0px";
  }
}

.slide-in {
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-name: move;
}

into

editable
import { styled } from "styletron-react";

export default () => {
  const SlideIn = styled("div", {
    animationDuration: "3s",
    animationIterationCount: "infinite",
    animationName: {
      from: {
        marginLeft: "150px"
      },
      to: {
        marginLeft: "0px"
      }
    }
  });
  return <SlideIn>This is animated.</SlideIn>;
};
This is animated.

Vendor Prefixes

What are vendor prefixes?

Browser vendors sometimes add prefixes to experimental or nonstandard CSS properties and JavaScript APIs, so developers can experiment with new ideas while—in theory—preventing their experiments from being relied upon and then breaking web developers' code during the standardization process.

Some of these experimental or nonstandard CSS properties are extremely useful and developers don't want to wait for their full standardization. Thus, they need to add correct vendor prefixes. Styletron uses inline-style-prefixer to do this for you! It dynamically detects your browser version and adds vendor prefixes when needed.

This is also pretty common technique used by CSS post/pre-processors. It's known as autoprefixer.

Adding vendor prefixes manually is verbose and error prone:

.boring {
  -webkit-transition: 200ms all linear;
  transition: 200ms all linear;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

Styletron and inline-style-prefixer takes care of it:

editable
import { styled } from "styletron-react";

export default () => {
  const Prefixed = styled("div", {
    transition: "200ms all linear",
    userSelect: "none",
    boxSizing: "border-box",
    display: "flex"
  });
  return (
    <Prefixed>
      Inspect this element in different browsers. Hey Safari. 😅
    </Prefixed>
  );
};
Inspect this element in different browsers. Hey Safari. 😅

Pitfalls

Shorthand And Longhand Properties

Many CSS properties can be defined through the shorthand syntax. For example

border-width: 2px;
border-style: solid;
border-color: black;

can be replaced by

border: 2px solid black;

You can use both shorthand and longhand properties in Styletron; however, you should never mix them in a single styled component!

This is perfectly fine in classic CSS

.content {
  border: 2px solid black;
  border-width: 5px;
}

since the order is always respected and border-width will be set to 5px. But if you do the same thing in Styletron

const Content = styled('div', {
  border: '2px solid black',
  borderWidth: '5px'
});

it's not clear if the border-width will be 5px or 2px. Why?

JavaScript object is an unordered collection of properties each of which contains a primitive value, object, or function.

The order is not deterministic. Also, Styletron splits each rule into a separate CSS class (Atomic CSS) and reuses same classes across multiple styled components. So if some other component already uses the borderWidth: '5px' rule and is mounted before the Content component, there is a great chance that you will end up with 2px since the order of in which CSS classes are defined matters and Styletron adds them incrementally as components are being mounted.

Media Queries Order

Media queries have a similar ordering problem as Shorthand And Longhand Properties. The order of media query definitions matters. Imagine these two scenarios:

(min-width: 480px)
(min-width: 600px)

and

(min-width: 600px)
(min-width: 480px)

If your viewport has 700px, those definitions would yield different results since they both get matched but only the last rule wins. Fortunately, Styletron (since [email protected]) is smart enough to automatically sort all used media queries in a mobile-first fashion. What does it mean? If your style object is:

{
  color: 'red',
  'screen and (min-width: 800px)': {
    color: 'blue'
  },
  'screen and (min-width: 420px)': {
    color: 'pink'
  },
}

Styletron will sort it as:

color: red
screen and (min-width: 420px)
screen and (min-width: 800px)

otherwise the 800px query would never get used since the 420px min-width query gets matched too and that's probably not what you would ever want. Since we can't sort mobile-first and desktop-first at the same time, Styletron does the mobile-first sorting by default. Please, don't use the desktop-first approach. You will run into issues.

Quotes

editable
import { styled } from "styletron-react";

export default () => {
  const Button = styled("button", {
    color: "white",
    backgroundColor: "white",
    "::before": {
      content: '"…"',
      color: "black",
      display: "block",
      position: "absolute"
    }
  });
  return <Button>xx</Button>;
};

This snippet works as expected. The lesson here is to not forget about double quotes when using properties as content since browsers expect that the resulting value will be surrounded by " " and the first pair of quotes is there just because of JavaScript.

This doesn't not work:

content: '…',