tsstyled
React visual primitives with first-class TypeScript support and a tiny footprint.
TL;DR: This library is a styled-components rewrite that is smaller, simpler, with stronger TypeScript types.
- Getting Started
- Motivation
- The Basics
- Theming
- Style syntax
- Style helpers
- Utilities
- Server Side Rendering (SSR)
- Ecosystem
Getting Started
Installation
Install the tsstyled
package and its react
peer dependency.
# With NPM
npm i tsstyled react
# With Yarn
yarn add tsstyled react
This library uses semantic versioning. Breaking changes will only be introduced in major version updates.
Compatibility
- React >= 16.14.0
- TypeScript >= 4.2.4
- IE >= 11
Community
Do you have questions, suggestions, or issues? Join the Discord server!
Motivation
The styled-components library is incredibly popular. The "visual primitives" pattern (ie. atomic components with styles included as code), combined with tagged templates, make it powerful and an easy transition from CSS/SCSS.
However, it was created many years ago, well before TypeScript had gained the popularity it has now. Types have now been added by the community. But, the API wasn't designed with types in mind, and some of the design choices are just not compatible with strong typing.
This library is a rewrite which keeps the key features, cleans up the design for TypeScript, and removes some unnecessary and counter productive "features". It also completely removes any dependence on third party libraries, and is about 1/3rd the size.
The problem(s) with styled-components
There are many relatively small but definite problems with the styled-components types, which are not fixable given the current design.
- Type errors are surfaced late, when you try to use the component, not when you using the
styled
function,attrs
method, or the tagged template. - It is possible to set the generics in ways that allow prop values to be assign without type errors, that will result in runtime errors.
- You can only add new props to a component's props type (not remove them).
- The types are messy, hard to parse, and break some type utilities (
React.ComponentProps
) - Setting generic types explicitly is hard to get right, because some orders of operation are reversed, and the type intersections are not the best solution for combining them.
- Theme typing is fragile, because it relies on declaration merging which doesn't always seem to work as intended. More importantly, using declaration merging locks all components into sharing a common theme type.
- See also the DefinitlyTyped bugs for styled-components.
Philosophically, the styled-components "re-styling" capability is too powerful. It can invalidate assumptions made by the originally styled component.
- The
as
property is allowed to change the base component to anything. This is lazy and incompatible with good design. If you want to implement your own component with anas
property, that’s completely fine, because you designed it that way. But having it automatically added to every styled component opens up too many uncertainties, and paves the way for a fragile component hierarchy. - The order of applying
attrs
is reversed. This is the opposite of standard component design. In vanilla React, if you wrap a component with another component, the outer component now controls what reaches the inner component, which is a good thing. If you restyle (wrap) an already styled component, the restyle gets to reach around the inner styled component and apply properties directly to the base component. This is fragile because you are modifying internal behaviors that you don't have visibility on or control over.
All of this means that when styling a component, you can’t assume you’re styling the component you think you are, you don’t really know what props will be passed to you, and your props might not be passed to the base component.
Moving from styled-components
Most styled-components capabilities (basic and advanced) are supported, with some notable differences and omissions.
- The
styled.div
syntax is not supported (onlystyled('div')
). - The
attrs
method is replaced byprops
, which can only be used once as the first chained method call afterstyled
, and it is applied in the intuitive (ie. functional) order when restyling (instead of the reverse order used byattrs
). - The template function returned by the
styled
function is not generic, because theprops
method is the only way to set the styled component's properties. - The
use
,set
, andmap
functions have been added to support stronger typing when manipulating property values, and they are also applied in the intuitive order likeprops
(instead of the reverse order used byattrs
). - The style object syntax is not supported, to keep the library size down, and because tagged templates provide a better developer experience overall.
- The as polymorphic prop is not supported, because it does not fit the philosophy of this library.
- The attributes passed through to simple HTML elements (eg.
div
) are not filtered based on known HTML attributes, but instead are filtered based on the following rules:- Props that start with
$
are always filtered out. - The
style
,children
, andref
props are never filtered out. - Function props are filtered out unless the prop name starts with
on
. - All other non-primitive (
string
,number
,boolean
) props are filtered out.
- Props that start with
- The component selector pattern only works when a component is given an explicit display name, because making every component selectable adds transfer size to the SSR output, and requiring a name can mitigate some potential SSR vs client rendering order gotchas.
- The API is similar enough to the styled-components API for the vscode-styled-components plugin to provide syntax support.
- No theme is automatically injected into styled component props, because custom themes can be manually injected by using a theme hook with the
use
method. - No keyframes utility is included, because the
@keyframes
at-rule can be used in any styled template string, and thegetId
utility can be used if animation name collisions are a concern. - No createGlobalStyle utility is included, because global styles can be created by calling
styled('style')
which produces a global style component. - No automatic vendor prefixing is performed, to keep library size and complexity down, and because it's unnecessary for most common styling scenarios.
The Basics
Style HTML elements
Use string tag names to create simple HTML elements with styling. The styled component supports all of the same props (included refs) that the HTML element supports.
import { styled } from 'tsstyled';
const StyledDiv = styled('div')`
color: red;
`;
Style React components
Any component which accepts a className
prop can be styled, just like an HTML tag. The styled component supports all of the same props (including refs) that the base component supports.
const Base = (props: { className?: string }): ReactElement => {
return <div className={props.className}>Foo</div>;
};
const StyledBase = styled(Base)`
color: red;
`;
Override component props type
When you style an HTML element or a React component, the new styled component has the exact same props as the original component by default. You can override that default by using the generic props
method to set a custom props type.
NOTE: The props
method must always be the first styled method called, and it can only be used once per styled component.
The simplest case is a props type which extends the base props type. New props can be added, prop values can be narrowed, and optional props can be removed.
const StyledDiv = styled('div').props<{
// Add a new prop.
$color?: string;
// Narrow the value type of a prop.
className?: 'foo' | 'bar';
// Remove an optional prop by narrowing the type to undefined.
style: undefined;
}>()`
color: ${(props) => props.$color};
`;
The styled component above accepts all of the intrinsic div
props, except style
which has been removed, and the className
only accepts the values foo
or bar
. It also accepts an extra non-standard $color
prop. Be sure to include the $
prefix if you want keep a property from being passed to the base HTML element as an attribute.
If you don't want to extend the base props, you can set the extend
option to false
.
interface IStyledDivExtraProps {
color?: string;
}
const StyledDiv = styled('div').props<{ $color?: string }>({ extend: false })`
color: ${(props) => props.$color};
`;
Now the styled component only accepts the $color
prop, and no other props.
If your custom props type is not "compatible" with the base component, then a mapping function to create props compatible with the base component is required. Compatible means that the new styled component props can be passed directly to the base component without modification. Changing a prop value type (eg. string
to string[]
), and removing a required prop, are examples of incompatible changes.
interface IStyledDivProps {
// The standard className can only be a string. Allowing a string
// array makes this incompatible with the base component (div)
// props.
className?: string | string[];
}
const StyledDiv = styled('div').props((props: IStyleDivProps) => ({
// This mapping function is required to convert an incompatible
// array class name back to a string which is compatible with the
// div.
className: props.className instanceof Array ? props.join(' ') : props.className
}))``;
Using the props
with a callback does not extend the base props. So, the above styled component now only supports the className
prop, and no other props. To keep any of the base component props, include them manually in your custom props type. The InferProps
type utility can be used to access the properties of any HTML element or React component.
import { InferProps } from 'tsstyled';
interface IStyledDivProps extends Omit<InferProps<'div'>, 'className'> {
className?: string | string[];
}
Provide default prop values
Default prop values can be provided for undefined props with the use
method. Props returned by the callback will only be set if the current prop value is undefined. This method is an alternative to using the map
method with Object.assign({}, defaultProps, props)
or { ...defaultProps, ...props }
, which allows explicitly undefined props to overwrite and hide default props.
const StyledDiv = styled('div').use((props) => ({
// Use the component ID as the default class name.
className: props.id,
}))`
color: red;
`;
Add or update prop values
Prop values can be added and modified (but not removed or set to undefined) with the set
method. Props returned by the callback will be set as long as the new value is defined. This method is an alternative to using the map
method with Object.assign({}, props, newProps)
or { ...props, ...newProps }
, which can allow explicitly undefined new prop values to overwrite and hide defined prop values.
const StyledDiv = styled('div').set((props) => ({
// Add a prefix to the existing class name.
className: props.className && `prefix-${props.className}`,
}))`
color: red;
`;
Map prop values
Prop values can be completely rewritten using the map
method. Props returned by the callback replace the current props without condition.
NOTE: Please give preference to the use
and set
methods. They provide better type support for their respective scenarios. Only use map
when you need to remove properties from the current props object.
const StyledDiv = styled('div').map((props) => ({
id: props.id,
className: props.className,
children: props.children,
// Props that are not returned, are removed.
}))`
color: red;
`;
Global styles
A global stylesheet can be added by styling a style
component.
const GlobalStyle = styled('style')`
body, html {
margin: 0;
padding: 0;
}
`;
render(
<>
<GlobalStyle />
<div>Page content</div>
</>
);
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, you may want to limit them to global styles, but they will work in any style. If you are concerned about names colliding, you can use the getId
utility to generate unique names.
import { styled, getId } from 'tsstyled';
const openSans = getId('Open Sans');
const slideIn = getId('slideIn');
const GlobalStyle = styled('style')`
@font-face {
font-family: ${openSans};
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
}
@keyframes ${slideIn} {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
`;
const StyledDiv = styled('div')`
font-family: ${openSans};
animation-name: ${slideIn};
`;
Theming
Styled components created with tsstyled
are not theme aware by default, and there is no default theme type or values. However, theming is trivial to setup, and requiring the extra setup makes it easier to support third party theme integration.
Create a theme
Themes are just values made available via a React context, and preferably using a React hook. The createTheme
utility is provided to make that a one step process. It accepts the default theme value, and returns a theme hook function and provider component.
import { createTheme } from 'tsstyled';
const [useTheme, ThemeProvider] = createTheme({
foregroundColor: black;
backgroundColor: white;
});
// The theme type can be inferred from the hook's return type.
type Theme = ReturnType<typeof useTheme>;
Use a theme
Themes can be integrated via React hooks in the props
, use
, set
, or map
method callbacks. Actually, any hook can be used in these callbacks (not just theme hooks) as long as hooks are not called conditionally. The recommended approach is to have hooks provide default values by calling them in a use
method callback.
const ThemedDiv = styled('div').use(() => ({
theme: useTheme(),
}))`
color: ${(props) => props.theme.foregroundColor};
background: ${(props) => props.theme.backgroundColor};
`;
Override theme values
The provider returned by the createTheme
utility allows the theme to be overridden for part of your React tree. It's not necessary to use the provider unless you want to override theme values.
const ThemeInvertedProvider = (props: { children?: ReactNode }) => {
return (
<ThemeProvider
value={(current) => ({
foregroundColor: current.backgroundColor,
backgroundColor: current.foregroundColor,
})}
>
{children}
</ThemeProvider>
);
};
render(
<ThemeInvertedProvider>
<ThemedDiv>Greetings from the dark side.</ThemedDiv>
</ThemeInvertedProvider>
);
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 (&
).
Simple CSS
If you just want to style one element, use CSS properties at the top-level (no surrounding block).
const StyledDiv = styled('div')`
color: red;
`;
These CSS properties will be wrapped in a block with a selector for the styled dynamic class.
._s7y13d {
color: red;
}
Child selectors
You can use CSS selectors and blocks to style children of the styled component.
const StyledDiv = styled('div')`
color: red;
.child {
color: blue;
}
`
The styled dynamic class will be automatically prepended to all selectors to make them "scoped".
._s7y13d {
color: red;
}
._s7y13d .child {
color: blue;
}
Nesting
You can nest blocks to create more complex selectors.
const StyledDiv = styled('div')`
color: red;
.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 {
color: red;
}
._s7y13d .child {
color: blue;
}
._s7y13d .child .grandchild {
color: green;
}
Parent selector references
As noted above, the parent selector is automatically prepended to child selectors. This behavior can be overridden by using a parent selector reference (&
) to inject the parent selector anywhere in your child selector. This includes injecting the parent selector multiple times to increase specificity, and is necessary when using pseudo selectors like :hover
which you will probably want to apply directly to the styled component instead of to its children.
const StyledDiv = styled('div')`
&& {
color: red;
}
&:hover {
color: blue;
}
.parent & {
color: green;
}
`
For any selector that contains an &
, the parent selector will replace the &
characters, and the parent selector will not be automatically prepended.
._s7y13d._s7y13d {
color: red;
}
._s7y13d:hover {
color: blue;
}
.parent ._s7y13d {
color: green;
}
At-rules
All CSS at-rules are supported (except @charset
which isn't allowed inside <style>
tags). Conditional group rules (ie. @media
and @supports
) which can have nested selector blocks, can themselves be nested, and can have deeply nested child selectors which use parent selector references.
const StyledDiv = styled('div')`
@media screen and (min-width: 900px) {
color: red
}
.child {
@media screen and (min-width: 600px) {
.grandchild {
color: blue;
.adopted & {
color: green;
}
}
}
}
`;
The 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;
}
}
Comments
Styles can contain both block (/* */
) and line comments (//
). Comments are never included in stylesheets.
const StyledDiv = styled('div')`
// This is a comment.
/* And so is...
this. */
`;
Empty values
If a CSS property value is an empty string or null-ish (null
, undefined
), then the whole property will be omitted from the style.
const StyledDiv = styled('div')`
color: ${null};
background: red;
`;
The color property is not included because it has no value.
._s7y13d {
background: red;
}
Style helpers
The css
utility provides syntax support when defining independent styles which are not immediately applied to a component.
Static helpers
Static helpers are simple style strings. As long as the template doesn't include any function values, the css
utility returns a string.
import { css, styled } from 'tsstyled';
const font: string = css`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: 1rem;
`;
// typeof font === 'string'
const StyledDiv = styled('div')`
${font}
color: red;
`;
Dynamic helpers
Dynamic helpers are functions that return style strings. If the template includes any function values, the css
utility returns a function instead of a string. The generic type defines the properties that your helper will accept.
const font = css<{ scale?: number }>`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: ${(props) => props.scale ?? 1}rem;
`;
// typeof font === 'function'
const StyledDiv = styled('div')`
${font({ scale: 2 })}
color: red;
`;
Themed helpers
If you're using theming, you'll almost certainly need some of your helpers to use the theme. Themed helpers are just a specialized form of dynamic helper which accepts the theme as a prop. However, it's a good idea not to depend on the entire theme, only the values you need. This makes it easier to use the helper in the case where you don't have the theme, and just want to manually fill in the required theme values.
const font = css<{ theme: Pick<Theme, 'fontSize'> }>`
font-family: Arial, sans-serif;
font-weight: 400;
font-size: ${(props) => props.theme.fontSize};
`;
const ThemedDiv = styled('div').use(() => ({ theme: useTheme() }))`
${font}
color: red;
`;
// Or, if you can't use the theme for some reason.
const StyledDiv = styled('div')`
${font({ theme: { fontSize: '1rem' } })}
color: red;
`;
Utilities
Detecting styled components
The isStyled
and isStyledSelector
functions can be used to detect components that have been styled. The isStyled
method detects any styled component, while the isStyledSelector
function only returns true for styled components that can be used as selectors.
import { isStyled, isStyledSelector } from 'tsstyled';
const StyledComp = styled('div')``;
const StyledCompAndSelector = styled('div', 'DisplayName')``;
isStyled(StyledComp); // true
isStyled(StyledCompAndSelector); // true
isStyledSelector(StyledComp); // false
isStyledSelector(StyledCompAndSelector); // true
Building a class name string
Joining multiple class names into a single string is a common problem. The classNames
function takes possibly falsy class names, or maps of class names, and returns a clean class names string.
import { isStyled, isStyledSelector } from 'tsstyled';
const className = classNames('a', { b: true, c: false }, null); // "a b"
Server Side Rendering (SSR)
No configuration is required to make SSR work. When rendered without a browser context (ie. a document
global), styles are rendered inline, before the first element that uses a style. On the client, tsstyled
will pull the inlined styles into the document <head>
before React rehydration occurs.
If the default SSR support using inlined styles doesn't work for your scenario, you can use the ServerStyleManager
and the StyleConfig
component to capture styles rendered during SSR.
import { ServerStyleManager, StyleConfig } from 'tsstyled';
const manager = new ServerStyleManager();
const html = renderToString(
<StyleConfig serverManager={manager}>
<App />
</StyleConfig>
);
const styles = manager.getStyleTag();
The getStyleTag
method returns an HTML string containing a single <style>
tag. There is also a getStyleElement
method that returns the same style as a React element, and a getStyleData
method that returns an array of raw style data ({ key: string, cssText: string }
).
Ecosystem
A list of some of the cool stuff built with tsstyled
. If you have something to share, please create a GitHub PR to add it to this README file.
Websites
- Your website here!
Packages
- Your package here!