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.
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
import { useStyletron } from "styletron-react"; export default () => { const [css] = useStyletron(); return ( <button className={css({ color: "red", fontSize: "16px", paddingLeft: "1em", paddingRight: "1em" })}> 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.
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:
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.
The style object can be nested so you can define media queries and most of pseudo classes:
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>; };
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.
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.
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:
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.
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.
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 /> </> ); };
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.
React hook useHover provides a nice concise API to have full control over hover states:
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.
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
import { styled } from "styletron-react"; export default () => { const FontLaila = styled("div", { fontFamily: { src: "url(/static/Laila.ttf)" } }); return <FontLaila>This is Laila font.</FontLaila>; };
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
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>; };
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:
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> ); };
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 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.
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: '…',