Basics
Styled
A high performance and easy to use CSS-in-JS library with support for server-side rendering. Built similar to other popular libraries, with a familiar API, providing a great overall developer experience.
Styles are defined using Object Literal syntax, meaning instead of writing properties in kebab-case
like regular CSS, they are in camelCase
eg: max-width
would be maxWidth
. Don't worry, Styled provides autocompletion and type checking for CSS properties and values thanks to CSSType.
There is also a way to define global styles using CSS, more on that later.
Installation
Install Styled using your favourite package manager:
# with npm
npm install @n3e/styled
# with yarn
yarn add @n3e/styled
# with pnpm
pnpm add @n3e/styled
# with bun
bun add @n3e/styled
Once installed, just import like so:
import styled from '@n3e/styled';
Getting started
Let's start by building a simple button:
const accent = '#ff6995';
const asRem = (
targetPxValue: number,
baseFontSize = 16
): string => `${targetPxValue / baseFontSize}rem`;
const Button = styled.button({
appearance: 'none',
backgroundColor: accent,
border: `2px solid ${accent}`,
borderRadius: asRem(4),
margin: 0,
transition: '0.2s linear'
});
const Composition = () => (
<Button>
I'm a button
</Button>
);
Pseudo
Styled comes with a helper for writing pseudo styles, below is an example on how to define pseudo-class
selectors:
import styled, { style } from '@n3e/styled';
const Button = styled.button({
// button base styles
[style.hover]: {
backgroundColor: 'rgba(255, 105, 180, 0.7)'
},
// pseudo class function
[style.not(style.disabled)]: {
backgroundColor: 'purple',
borderColor: 'purple',
color: 'white'
},
// you can write it yourself, if preferred
':hover': {
backgroundColor: 'rgba(255, 105, 180, 0.7)'
},
':not(:disabled)': {
backgroundColor: 'purple',
borderColor: 'purple',
color: 'white'
}
});
const Composition = () => (
<Button>
I'm a button
</Button>
);
Similarly pseudo-element
selectors are defined as shown below:
const PrefixedUsingBefore = styled.span({
[style.before]: {
content: '$'
},
// the same can be done manually
'::before': {
content: '$'
}
});
const Composition = () => (
<PrefixedUsingBefore>
dollar bucks
</PrefixedUsingBefore>
);
Combinator
Descendant combinator
const Paragraph = styled.p({
margin: 0
});
const RichText = styled.div({
[style.selector('p')]: {
margin: '0 0 24px'
},
// target a Styled component
[style.selector(Paragraph)]: {
margin: '0 0 24px'
}
});
const Composition = () => (
<RichText>
<p>Para one</p>
<Paragraph>Para two</Paragraph>
</RichText>
);
Child combinator (>)
const Image = styled.img({
display: 'block',
maxWith: '100%'
});
const RichText = styled.div({
[style.selector('> img')]: {
border: '2px solid yellow'
},
// target a Styled component
[style.selector(`> ${Image}`)]: {
border: '2px solid yellow'
}
});
const Composition = () => (
<RichText>
<img src='path/to/image' />
<Image src='path/to/image' />
</RichText>
);
Sibling combinators (~ or +)
const Label = styled.label({
display: 'inline-block',
cursor: 'pointer',
verticalAlign: 'top'
});
const Checkbox = styled.input({
[style.checked]: {
[style.selector('+ label')]: {
fontWeight: 'bold'
},
// target a Styled component
[style.selector(`+ ${Label}`)]: {
fontWeight: 'bold'
}
}
});
const Composition = () => (
<>
<Checkbox type='checkbox' />
<Label>Label text</Label>
</>
);
Attribute selectors
const Anchor = styled.a({
// base styles
[style.attribute('href').startsWith(
'http',
'https'
)]: {
// styles ...
},
// you need to use Styled's combinator helper `or` to create
// nested selector lists (ie: comma separated selector)
[style.or(
'[href^="http"]',
'[href^="https"]'
)]: {
// styles ...
},
// or have to duplicate style definitions
'[href^="http"]': { /* styles ... */ },
'[href^="https"]': { /* styles ... */ }
});
const Composition = () => (
<Anchor href='#'>
I'm an anchor
</Anchor>
);
At-rules
Nested at-rules such as @media
, @supports
and even @container
are declared as demonstrated below:
const Container = styled.div({
// base styles
'@media screen and (min-width: 576px)': {
maxWidth: '540px'
},
'@media screen and (min-width: 768px)': {
maxWidth: '720px'
},
// you can even nest at-rules
'@supports (display: flex)': {
display: 'flex',
flexDirection: 'column',
'@media screen and (min-width: 768px)': {
flexDirection: 'row'
}
}
});
For examples of @keyframes
and @font-face
, refer to API section.
Styled exposes another helper for building media queries.
Prop based styles
Property matching or pattern matching can be achieved by calling the prop
method on the style
helper.
const Anchor = styled.a({
display: 'inline-block',
textTransform: 'uppercase',
cursor: 'pointer',
[style.prop('isActive')]: {
color: '#ff6995',
textDecoration: 'none'
}
});
const Composition = () => (
<Anchor href='#' isActive>
I'm an anchor
</Anchor>
);
You can even specify a function that takes the prop being matched as its only argument and also type the props for the same.
type IconProps = {
width?: number;
height?: number;
};
const Icon = styled.svg<IconProps>({
display: 'inline-block',
stroke: 'transparent',
fill: 'currentColor',
verticalAlign: 'middle',
pointerEvents: 'none',
cursor: 'inherit',
[style.prop('width')]: (width: number) => ({
width: `${width}px`
}),
[style.prop('height')]: (height: number) => ({
height: `${height}px`
})
});
const Composition = () => (
<Icon width={36} height={36} />
);
IMPORTANT!In order to avoid maintaining a whitelist of props that are not meant to be passed down to the underlying HTML tag, all props used in any of the pattern matching functions will be treated as transient props and will NOT flow downward.
Thus in the example above
width
andheight
props will not be passed to the underlyingsvg
tag, so ensure styles are defined to handle that.
To match against multiple properties, just use one of all
, any
or not
methods on the style.props
helper.
type CheckboxUIProps = {
isChecked?: boolean;
isDisabled?: boolean;
};
const CheckboxUI = styled.span<CheckboxUIProps>({
[style.props.all('isDisabled', 'isChecked')]: {
opacity: 0.7
}
});
In the above example, opacity styles will only be applied when both isChecked
and isDisabled
props are supplied to the CheckBoxUI
component.
When using a function for matching multiple props, be sure to pass in all props or the props object if not destructuring.
type ButtonProps = {
borderColour?: string;
borderStyle?: 'solid' | 'dashed' | 'none';
};
const Button = styled.button<ButtonProps>({
[style.props.any('borderColour', 'borderStyle')]: ({
borderColour,
borderStyle
}: ButtonProps) => ({
borderColor: borderColour || 'purple',
borderStyle: borderStyle || 'solid'
})
});
Generics
Not to confuse with Typescript's Generics, there is a mechanism for defining Components that are "generic", basically a Higher Order Component that takes a Component
as its only argument.
To define a Generic component, simply call the generic
method as shown below:
type SizeProp = {
size?: number;
};
const GenericSize = styled.generic<SizeProp>({
boxSizing: 'border-box',
[style.prop('size')]: (size: number) => ({
fontSize: `${size / 16}rem`,
lineHeight: `${size / 16 * 1.5}rem`
})
});
Then wrap a component with it as shown below.
const Heading = styled.h1({
// styles ...
});
const Wrapped = GenericSize(Heading);
const Composition = () => (
<Wrapped size={64}>
Wrapped Heading Component
</Wrapped>
);
Generic components will pass all props received down to the wrapped component. Ensure that the className
prop is passed down to the underlying DOM node when using custom components.
type CustomHeadingProps = {
children?: React.ReactNode;
className?: string;
};
const CustomHeading = ({
children,
className
}: CustomHeadingProps) => (
<h2 className={className}>
{children}
</h2>
);
const Wrapped = GenericSize(CustomHeading);
const Composition = () => (
<Wrapped size={64}>
Wrapped CustomHeading Component
</Wrapped>
);
To avoid nesting when trying to use multiple generic components, simply call the extend
method.
const GenericContentParadigm = styled.generic({
margin: '0 0 1.5rem',
[style.lastChild]: {
marginBottom: 0
}
});
const GenericHugeText = styled.generic({
fontSize: '4.5rem',
lineHeight: '5rem'
});
const PageHeading = styled
.h1({
color: '#333',
fontWeight: 400,
padding: 0
})
.extend(
GenericContentParadigm,
GenericHugeText
);
You can also extend from other Styled Components and even plain object literal styles. The extend
method performs a deep merge, so nested styles containing duplicate keys will be combined.
const GenericClearFloat = styled.generic({
float: 'left',
[style.after]: {
content: '',
clear: 'both',
display: 'block'
}
});
const Danger = styled.span({
color: 'red',
[style.after]: {
content: '!'
}
});
const ExtendMayhem = styled
.div({
color: 'blue',
[style.hover]: {
textDecoration: 'underline',
[style.after]: {
color: 'pink',
cursor: 'pointer'
}
}
})
.extend(
Danger,
GenericClearFloat,
{
[style.hover]: {
textDecoration: 'none',
[style.after]: {
cursor: 'default'
}
}
}
);
The resulting object styles for <ExtendMayhem>
will be
{
float: 'left',
color: 'red',
[style.after]: {
content: '',
clear: 'both',
display: 'block'
},
[style.hover]: {
textDecoration: 'none',
[style.after]: {
color: 'pink',
cursor: 'default'
}
}
}