Minimal CSS-in-JS styled components solution for React.
Minimal CSS-in-JS styled components solution for React.
There are also some things that are non-goals.
as
property, .withComponent()
method)
.attrs()
method)
defaultProps
.import { styled } from '@minstack/styled';
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.
const StyledComponent = styled('div')`
color: black;
`;
The tag name method style is also supported.
const StyledComponent = styled.div`
color: black;
`;
Style any React component which accepts a className
property, or extend the styles of an already styled component.
const StyledComponent = styled(Component)`
color: black;
`;
Extra properties can be added to the styled component by setting the generic parameter of the template string. Generally, style properties should be prefixed 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};
`;
Use the styled.global
utility to create global style components.
const GlobalStyle = styled.global`
body,
html {
margin: 0;
padding: 0;
}
`;
Style properties can be added to global styles too.
interface GlobalStyleProps {
$font?: string;
}
const GlobalStyle = styled.global<GlobalStyleProps>`
body,
html {
font-family: ${(props) => props.$font};
}
`;
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('font/open-sans');
const slideInAnimation = getId('keyframes/slide-in');
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};
`;
Pass a theme hook (or any function) which returns a theme to the createStyled
utility. The theme value will then be available as the second argument passed to any styled template string functional value.
// File: styled-with-theme.ts
import { createStyled } from '@minstack/styled';
export const styled = createStyled(useTheme);
This creates a strongly typed styled
instance. Use this instance instead of the built-in instance.
import { styled } from './styled-with-theme';
const ThemedComponent = styled('div')`
color: ${(props, theme) => theme.fgColor};
background-color: ${(props, theme) => theme.bgColor};
`;
All of CSS plus nesting is supported.
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
._rmsds7y13d_ {
color: red;
}
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".
._rmsds7y13d_ .child {
color: blue;
}
Every styled component (except global styles) can be used as a selector.
const StyledComponentA = styled('div')`
color: blue;
`;
const StyledComponentB = styled('div')`
${StyledComponentA} {
background-color: yellow;
}
`;
Each styled component has a unique static class which is generated on creation. The styled component's toString()
method returns a selector string (eg. "._rmsss7y13d_"
) for that static class.
._rmsds7y13d_ ._rmsss7y13d_ {
color: red;
}
The static class is generated from the component's display name, the static part of the style template, inherited static classes (when extending another styled component), and the number of previously created components that share the same "thumbprint". In most cases, this should make static classes stable across SSR and client renders. If static class SSR problems occur, it's probably due to components with the same fingerprint having an unstable creation order. Try changing the displayName
using the .withConfig()
method to make the problematic component's fingerprint unique.
const StyledComponent = styled.div.withConfig({ displayName: 'StyledComponent' })`
color: red;
`;
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.
._rmsds7y13d_ .child {
color: blue;
}
._rmsds7y13d_ .child .grandchild {
color: green;
}
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;
}
`;
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) {
._rmsds7y13d_ {
color: red;
}
}
@media screen and (min-width: 600px) {
._rmsds7y13d_ .child .grandchild {
color: blue;
}
.adopted ._rmsds7y13d_ .child .grandchild {
color: green;
}
}
If a CSS property value is "empty" (empty string, false
, null
, undefined
, or ""
), 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.
._rmsds7y13d_ {
background-color: red;
}
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. */
`;
The styled.string
tagged template function returns a simple style string with all values interpolated. Only static values are allowed (no functions). Empty property values (null
, undefined
, false
, and ""
) work the same way they do in styled components, and cause the property to be omitted.
const fontHelper = styled.string`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: ${size};
`;
// Then use in a styled component or another helper.
const StyledComponent = styled('div')`
${fontHelper}
color: red;
`;
The styled.string
helper has no side effects and does very little work, so it's also safe to use in functions.
const shadow = (depth: number) => {
return styled.string`
-moz-box-shadow: 0 ${depth}px ${depth}px black;
-webkit-box-shadow: 0 ${depth}px ${depth}px black;
box-shadow: 0 ${depth}px ${depth}px black;
`;
};
// Then use in a styled component or another helper.
const StyledComponent = styled('div')<{ $shadowDepth: number }>`
${(props) => shadow(props.$shadowDepth)}
color: red;
`;
Use the StyledTest
wrapper to produce snapshots with stable class names and style information.
const container = render(<MyStyledComponent />, { wrapper: StyledTest });
expect(container).toMatchSnapshot();
// Snapshot
<div>
<div
class="_test-dynamic-0_ _test-static-0_"
>
Hello, world!
</div>
<style>
._test-dynamic-0_ {
padding: 1rem;
}
</style>
</div>
A StyledProvider
can override the default cache
, manager
, and renderer
. No provider is required for default operation.
const cache = createStyledCache();
const manager = createStyledManager();
const renderer = createStyledRenderer();
render(
<StyledProvider cache={cache} manager={manager} renderer={renderer}>
<App />
</StyledProvider>,
);
The StyledTest
component is a pre-configured StyledProvider
which injects test versions of all three resources to replace class names and capture styles.
Note: The provided cache, manager, and renderer must not change over the lifetime of a styled component. An error will be thrown (or logged in production) if they mutate.
Use createSsrStyledManager
and the StyledProvider
to capture styles when rendering the application on the server.
const manager = createSsrStyledManager();
const html = renderToString(
<StyledProvider manager={manager}>
<App />
</StyledProvider>,
);
const html = `
<!doctype HTML>
<html>
<head>
${manager.getStyleTags()}
</head>
<body>
<div id="root">
${html}
</div>
</body>
</html>
`;
The SSR manager's getStyleTags()
method returns a single html string containing only <style>
tags. There are also getStyleElement()
(React elements array) and getCss()
(css strings array) methods.
Use createStyledManager
(or createSsrStyledManager
) and the StyledProvider
to set a nonce
on all injected styles.
const manager = createStyledManager(nonce);
render(
<StyledProvider manager={manager}>
<App />
</StyledProvider>,
);
Feature | MinStack Styled | Goober | Styled Components | Emotion | |
---|---|---|---|---|---|
Library | |||||
Bundle size (approx. kB)[1] | 2.8 | 1.2 | 13.3 | 9.1 | |
Zero dependencies | 🟢 | 🟢 | 🔴 | 🔴 | |
Typescript native | 🟢 | 🟢 | 🔴 | 🟢 | |
API | |||||
Tagged template styles | 🟢 | 🟢 | 🟢 | 🟢 | |
Dynamic styles | 🟢 | 🟢 | 🟢 | 🟢 | |
Object styles | 🔴 | 🟢 | 🟢 | 🟢 | |
Global styles | 🟢 | 🟢 | 🟢 | 🟢 | |
Polymorphism (as ) |
🔴 | 🟢 | 🟢 | 🟢 | |
Property mapping (attrs ) |
🔴 | 🔴 | 🟢 | 🔴 | |
Theming [2] | 🟢 | 🟡 | 🟡 | 🟡 | |
SSR | 🟢 | 🟢 | 🟢 | 🟢 | |
Snapshot testing | 🟢 | 🔴 | 🟢 | 🟢 | |
Style | |||||
Basic CSS syntax [3] | 🟢 | 🟡 | 🟢 | 🟢 | |
CSS @media |
🟢 | 🟢 | 🟢 | 🟢 | |
CSS @keyframes |
🟢 | 🟢 | 🟢 | 🟢 | |
CSS @font-face |
🟢 | ╠| ╠| 🟢 | |
CSS @import |
🟢 | ╠| 🔴 | 🟢 | |
Other CSS @ rules |
🟢 | ╠| ╠| ╠| |
Vendor prefixing [4] | 🔴 | 🟡 | 🟢 | 🟢 | |
Rule nesting | 🟢 | 🟢 | 🟢 | 🟢 | |
Parent selectors (& ) |
🟢 | 🟢 | 🟢 | 🟢 | |
Styled component selectors [5] | 🟢 | 🟡 | 🟢 | 🟢 |
Â
styled
export (after tree-shaking, minification, and gzip) calculated using the Webpack bundle analyzer.Goober is very similar to this solution. It's just as fast, smaller, and has support for a few extra feature (object styles, and the as
property). So what are Goober's downsides, and why would I use this instead?
StyledTest
wrapper component which not only enables snapshot testing, but does it in a way that is test framework agnostic.setup()
function which configures the single global instance of the API, and this does not change the theme type. Extending the theme type can be accomplished with declaration merging, but this is again global and not very type safe. This library provides the createStyled()
factory that returns a new API instance, which has a strongly typed theme.styled.div
instead of styled('div')
). This library supports styled.<tag>
without compile time support.setup()
when using React. This library targets React and requires preact/compat
when using Preact.This library is opinionated and leaves out some features that Goober supports. This is to reduce the number of alternative ways that styled components can be designed, which increases code consistency, and provides an overall better developer experience (DX). Removing support for two different ways to accomplish the same thing also means the library size and runtime overhead are reduced and/or allocated to improved core features, and that the library is more maintainable overall.
as
property for changing the underlying component type of the styled component. This library does not because it is inherently type unsafe, and using style helpers (eg. the styled.string
utility) provides a better way to reuse styles.See the benchmark.js script for the benchmark implementation.
Library | Op/s |
---|---|
MinStack Styled | 144,970 |
Goober | 142,028 |
Emotion | 124,681 |
Styled Components | 118,072 |
getId
accepts an optional namespace argument (re-added).withConfig()
static method to styled templatesgetId
and dynamic class hashesstyled.div
alternative to styled('div')
)useInsertionEffect
when availablestyled.string
helper for building static style stringsStyledProvider
createSsrStyledManager
)StyledTest
)getId
no longer accepts an argumentstyled.mixin
renderStylesToString