styled
styled copied to clipboard
Minimal CSS-in-JS styled components solution for React.
tsstyled
A small, fast, and simple CSS-in-JS styled components solution for React, written in Typescript.
- Small: Less than 4kb (minified and gzipped) and zero dependencies.
- Fast: Similar to other styled component solutions (benchmarks included).
- Simple: Minimal/Opinionated API creates a great developer experience.
- Typed: Written in Typescript. Designed for Typescript.
- Goals
- Getting Started
- Style properties
-
Global styles
- Defining keyframes and fonts
-
Theming
- Creating a theme
- Using a theme
-
Style syntax
- Styling self
- Styling children
- Styling other styled components
- Nesting rules
- Using parent selector references
- Using at-rules
- Using empty values
- Commenting
-
Style mixins (helpers)
- Creating simple mixins
- Creating parametric mixins
- Server Side Rendering (SSR)
- Testing
-
Comparison
- Benchmarks
Goals
- Small bundle size and zero dependencies
- Simple and opinionated API, with high quality types.
- Type-safe themes without declaration merging
- Future-proof CSS support
- Server side rendering
- Compatibility
- React >= 16.14.0
- ES2017/ES8 (eg. recent versions of Chrome, Edge, Safari, and Firefox)
- Webpack tree-shakable
There are also some things that are non-goals. They were considered, and then the choice was made to explicitly not include support for them.
- No auto vendor prefixing
- No object styles
- No component polymorphism (eg.
as
property,withComponent
method) - No "non-style" features (eg.
.attrs()
method) - No React Native support
Getting Started
Install the tsstyled
package and its react
peer dependency.
npm add tsstyled react
yarn add tsstyled react
Create the styled API. Usually this is done only once per package.
import { createStyled } from 'tsstyled';
const styled = createStyled();
Style any HTML element type by using the tag name. The styled component supports all of the same props (included refs, which are forwarded) that the HTML element supports. Styling basic HTML elements is what you should be doing most of the time.
const StyledComponent = styled('div')`
color: black;
`;
Style any React component which accepts a className
property. This comes with more tech debt than styling HTML elements, because you can't be sure how the style class is being applied inside the component. This should be done rarely and with careful consideration.
const StyledComponent = styled(Component)`
color: black;
`;
Extend the styling of an already styled component. Just like you want to keep your CSS specificity as low as possible, you also want to avoid trying to "patch" an already styled component. Styles can be dynamic, which means applying overrides can quickly get complicated. This should be an absolute last resort.
const ReStyledComponent = styled(StyledComponent)`
color: gray;
`;
Style properties
You can add extra properties to the styled component by setting the generic parameter of the template string. Generally, you should prefix style properties with $
to indicate that they are only used for styling. Any property name which starts with the $
character will not be passed through to the underlying HTML element as an attribute.
interface ComponentStyleProps {
$font?: string;
}
const StyledComponent = styled('div')<ComponentStyleProps>`
font-family: ${(props) => props.$font};
`;
Global styles
Use the styled.global
utility to create global style components.
const GlobalStyle = styled.global`
body,
html {
margin: 0;
padding: 0;
}
`;
You can add style properties to global styles too.
interface GlobalStyleProps {
$font?: string;
}
const GlobalStyle = styled.global<GlobalStyleProps>`
body,
html {
font-family: ${(props) => props.$font};
}
`;
Defining keyframes and fonts
Defining keyframes or font-faces is the same as defining any other style. Since they are not scoped to any particular component, they should probably only be used in global styles. To prevent name collisions, use the included getId
utility to generate CSS-safe unique names.
const openSansFont = getId('Open Sans');
const slideInAnimation = getId('slideIn');
const GlobalStyle = styled.global`
@font-face {
font-family: ${openSansFont};
src: url('/fonts/OpenSans-Regular-webfont.woff') format('woff');
}
@keyframes ${slideInAnimation} {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
`;
const StyledComponent = styled('div')`
font-family: ${openSansFont};
animation-name: ${slideInAnimation};
`;
Theming
A theme factory is provided instead of a single built-in theme. This allows themes to be strongly typed without relying on Typescript declaration merging (which does not provide great type-safety).
Creating a theme
A theme is essentially a context which provides theme constants. The createReactTheme
utility makes it easy to create that kind of context, returning a hook for theme access, and a provider for theme overriding.
const [useTheme, ThemeProvider] = createReactTheme({
fgColor: 'black';
bgColor: 'white';
});
Note: The createReactTheme
helper is only for convenience. Any hook could potentially be used, including theme hooks from other libraries.
Using a theme
Pass the a hook (or any function) which returns a theme value to the createStyled
function. The theme value will then be available as the second argument passed to any styled template string functional value.
const styled = createStyled(useTheme);
const ThemedComponent = styled('div')`
color: ${(props, theme) => theme.fgColor};
background-color: ${(props, theme) => theme.bgColor};
`;
Style syntax
Style syntax is CSS-like, and all CSS properties, selectors, and at-rules are supported. In addition, SCSS-like nesting is supported with parent selector references (&
).
Styling self
To apply styles directly to the HTML element or component being styled, use CSS properties at the top-level of the tagged template (no surrounding block).
const StyledComponent = styled('div')`
color: red;
`;
Top-level CSS properties will be wrapped in a dynamic styled class selector
._s7y13d {
color: red;
}
Styling children
Use CSS rule blocks to style children of the styled component.
const StyledComponent = styled('div')`
.child {
color: blue;
}
`;
The styled dynamic class will be automatically prepended to all selectors to make them "scoped".
._s7y13d .child {
color: blue;
}
Styling other styled components
Every styled component (except global styles) can be used as a selector for that specific styled component.
const StyledComponentA = styled('div')`
color: blue;
`;
const StyledComponentB = styled('div')`
${StyledComponentA} {
background-color: yellow;
}
`;
The styled component's toString()
method returns a unique selector string (eg. ".tss_s7y13d"
) which matches that specific styled component.
._s7y13d .tss_s7y13d {
color: red;
}
Nesting rules
Nest rule blocks to create more complex selectors.
const StyledComponent = styled('div')`
.child {
color: blue;
.grandchild {
color: green;
}
}
`;
Just like the styled dynamic class is prepended to top-level selectors, so too are parent selectors prepended to child selectors.
._s7y13d .child {
color: blue;
}
._s7y13d .child .grandchild {
color: green;
}
Using parent selector references
Parent selector references (&
) work the same way they do in SCSS/SASS. The one extra detail is that when a parent selector is used at the style root (not nested inside a parent block), it refers to the unique style class of the current style, which is the implicit/virtual parent block selector for the style.
const StyledComponent = styled('div')`
&& {
color: red;
}
&:hover {
color: blue;
}
.parent & {
color: green;
}
`;
Using at-rules
All CSS at-rules are supported (except @charset
which isn't allowed inside <style>
elements).
const StyledComponent = styled('div')`
@media screen and (min-width: 900px) {
color: red;
}
.child {
@media screen and (min-width: 600px) {
.grandchild {
color: blue;
.adopted & {
color: green;
}
}
}
}
`;
At-rules will be hoisted as necessary, and parent selectors will be handled the same way they would be without the intervening at-rule.
@media screen and (min-width: 900px) {
._s7y13d {
color: red;
}
}
@media screen and (min-width: 600px) {
._s7y13d .child .grandchild {
color: blue;
}
.adopted ._s7y13d .child .grandchild {
color: green;
}
}
Using empty values
If a CSS property value is "empty" (empty string, false
, null
or undefined
), then the whole property will be omitted from the style.
const StyledComponent = styled('div')`
color: ${null};
background-color: red;
`;
The color property is not included because it has no value.
._s7y13d {
background-color: red;
}
Commenting
Styles can contain both block (/* */
) and line comments (//
). Comments are never included in rendered stylesheets.
const StyledComponent = styled('div')`
// This is a comment.
/* And so...
...is this. */
`;
Style mixins (helpers)
The styled.mixin
tagged template utility returns a mixin (AKA: helper) function. When the returned function is called, it returns a style string with all values interpolated.
Creating simple mixins
Mixins do not accept any parameters by default.
const fontMixin = styled.mixin`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: 1rem;
`;
const StyledComponent = styled('div')`
color: red;
${fontMixin}
`;
Creating parametric mixins
Helpers which accept parameters can be created by setting the generic parameter of the styled.mixin
template string.
interface FontMixinProps {
scale?: number;
}
const fontMixin = styled.mixin<FontMixinProps>`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: ${(props) => props.scale || 1}rem;
`;
const StyledComponent = styled('div')`
${fontMixin({ scale: 2 })}
color: red;
`;
Server Side Rendering (SSR)
During SSR, there is no DOM and therefore no document
global. TSStyled detects this and uses a very minimal "virtual" DOM behind the scenes. So, after rendering the document body, use the renderStylesToString
utility to get all of the <style>
elements (generated by TSStyled components) as an HTML string.
const appHtml = renderToString(<App />);
const stylesHtml = renderStylesToString();
const html = `
<!doctype HTML>
<html>
<head>
${stylesHtml}
</head>
<body>
<div id="root">${appHtml}</div>
</body>
</html>
`;
Testing
During testing, there may be a DOM (eg. jsdom) and a document
global. However, the NODE_ENV
environment variable should also be set to test
(Jest sets this automatically). If it is, the SSR implementation is used. So, you can use the same renderStylesToString
utility to test (eg. Jest snapshots) your styles.
expect(renderStylesToString()).toMatchSnapshot();
Comparison
TSStyled compared to other styled component solutions.
- 🟢 Supported
- 🟡 Partially supported
- 🔴 Not supported
- ⭕ Not documented
Feature | TSStyled | Goober | Styled Components | Emotion | |
---|---|---|---|---|---|
Library | |||||
Bundle size (approx. kB) | 4 | 2 | 13 | 8 | |
Zero dependencies | 🟢 | 🟢 | 🔴 | 🔴 | |
Typescript native | 🟢 | 🟢 | 🔴 | 🟢 | |
API | |||||
Tagged template styles | 🟢 | 🟢 | 🟢 | 🟢 | |
Object styles | 🔴 | 🟢 | 🟢 | 🟢 | |
Global styles | 🟢 | 🟢 | 🟢 | 🟢 | |
Polymorphism (as ) |
🔴 | 🟢 | 🟢 | 🟢 | |
Property mapping (attrs ) |
🔴 | 🔴 | 🟢 | 🔴 | |
Theming [1] | 🟢 | 🟡 | 🟡 | 🟡 | |
SSR | 🟢 | 🟢 | 🟢 | 🟢 | |
Style | |||||
CSS @media |
🟢 | 🟢 | 🟢 | 🟢 | |
CSS @keyframes |
🟢 | 🟢 | 🟢 | 🟢 | |
CSS @font-face |
🟢 | ⭕ | ⭕ | 🟢 | |
CSS @import |
🟢 | ⭕ | 🔴 | 🟢 | |
Other CSS @ rules |
🟢 | ⭕ | ⭕ | ⭕ | |
Vendor prefixing [2] | 🔴 | 🟡 | 🟢 | 🟢 | |
Rule nesting | 🟢 | 🟢 | 🟢 | 🟢 | |
Parent selectors (& ) |
🟢 | 🟢 | 🟢 | 🟢 | |
Style mixins [3] | 🟢 | 🟡 | 🟢 | 🟢 | |
Styled component selectors | 🟢 | 🟢 | 🟢 | 🟢 |
- [1] Goober, Styled Components, and Emotion, all support only a single theme, which must be typed using declaration merging.
- [2] Goober provides vendor prefixing as an additional package.
- [3] Goober doesn't provide a
css
utility for creating mixins, but it does support function values in tagged templates.
Benchmarks
The benchmark app is available online, or by cloning the TSStyled repository and running the npm start
command.