@dark-engine/styled
v1.4.2
Published
Styled components for Dark
Downloads
28
Maintainers
Readme
@dark-engine/styled 💅
Styled components for Dark 🌖
Features
- 📚 Comprehensive CSS support
- 🎁 Encapsulated scope
- 🎉 Accommodation of global styles
- 📝 Styles dictated by component properties
- 🔁 Reusable fragments
- 🛒 CSS nesting
- 🎨 Theming
- 💃 CSS Animations
- 💽 SSR
- 🗜️ Minification
- ✂️ No deps
- 📦 Small size (6 kB gzipped)
Usage
const Button = styled.button<{ $isPrimary?: boolean } & DarkJSX.Elements['button']>`
display: inline-block;
font-size: 1rem;
padding: 0.5rem 0.7rem;
background-color: var(--color);
color: var(--text-color);
border: 1px solid var(--color);
border-radius: 3px;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--hover-color);
}
&:active {
background-color: var(--color);
}
${p => css`
--color: ${p.$isPrimary ? '#BA68C8' : '#eee'};
--hover-color: ${p.$isPrimary ? '#8E24AA' : '#E0E0E0'};
--text-color: ${p.$isPrimary ? '#fff' : '#000'};
`}
`;
<Button>Normal</Button>
<Button $isPrimary>Primary</Button>
Installation
npm:
npm install @dark-engine/styled
yarn:
yarn add @dark-engine/styled
CDN:
<script src="https://unpkg.com/@dark-engine/styled/dist/umd/dark-styled.production.min.js"></script>
API
import {
type StyledComponentFactory,
type StyleSheet,
type Keyframes,
ThemeProvider,
createGlobalStyle,
keyframes,
useTheme,
useStyle,
styled,
css,
VERSION,
} from '@dark-engine/styled';
import { ServerStyleSheet } from '@dark-engine/styled/server';
Getting Started
The styled uses tagged template literals to describe styles and create a final styled component that can be rendered like a regular Dark component, which can be nested with children and passed props. Under the hood, styled parses the style string into the simple abstract syntax tree (AST) in one pass, which is then transformed into final CSS and inserted into the DOM. At the same time, styles are divided into static and dynamic (based on props) for greater fragmentation of reused CSS classes. CSS classes are generated based on a fast non-cryptographic hash function and attached to DOM nodes.
const Layout = styled.section`
padding: 4em;
background: papayawhip;
`;
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: #BF4F74;
`;
return (
<Layout>
<Title>
Hello World!
</Title>
</Layout>
);
// in the DOM
<section class="dk-bgjhff">
<h1 class="dk-bbigce">Hello World!</h1>
</section>
Working with dynamic styles
To transmit dynamic data, it is recommended to use props names that begin with $
, if these properties should not end up in the attributes of the DOM node.
const Box = styled.div<{ $color: string } & DarkJSX.Elements['div']>`
width: 100px;
height: 100px;
background-color: ${p => p.$color};
`;
<Box $color='red' />
<Box $color='green' />
<Box $color='blue' />
If you need to dynamically generate something more than just a style property value, then you need to always use a special css
function that converts the style string to the AST.
const Box = styled.div<{ $color: string } & DarkJSX.Elements['div']>`
width: 100px;
height: 100px;
${p => css`
background-color: ${p.$color};
`}
`;
Reusable CSS fragments
Fragments make coding easier and prevent style elements from being repeated.
const square = (size: number) => css`
width: ${size}px;
height: ${size}px;
`;
const Box = styled.div`
${square(100)}
background-color: blue;
`;
Extending styles
To reuse already written styles, you can wrap a ready-made styled component in a styled
function. In this case, a new component will be created that will combine all the styles of the parent component with its own styles, which will take precedence.
const Button = styled.button`
display: inline-block;
color: #bf4f74;
font-size: 1rem;
padding: 0.25rem 1rem;
border: 2px solid #bf4f74;
border-radius: 3px;
`;
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
<Button>Normal button</Button>
<TomatoButton>Tomato button</TomatoButton>
You can also style any arbitrary component using this approach. The only requirement is that it passes a class
or className
prop to the desired DOM node.
type SomeButtonProps = {
className?: string;
slot: DarkElement;
};
const SomeButton = component<SomeButtonProps>(({ className, slot, ...rest }) => {
return (
<button {...rest} class={[className, 'btn'].filter(Boolean).join(' ')}>
{slot}
</button>
);
});
const Button = styled(SomeButton)`
display: inline-block;
color: #bf4f74;
font-size: 1rem;
padding: 0.25rem 1rem;
border: 2px solid #bf4f74;
border-radius: 3px;
`;
<Button>Button with .btn class</Button>
Dynamically swap tags or components
In some cases, you may need to replace the tag or component that needs to be rendered. For example, replace the button
tag with the a
tag, while maintaining the style of the button. There is prop as
for this.
const Button = styled.button`
display: inline-block;
color: #bf4f74;
font-size: 1rem;
padding: 0.25rem 1rem;
border: 2px solid #bf4f74;
border-radius: 3px;
text-decoration: none;
`;
<Button>Normal button</Button>
<Button as='a' {...{ href: 'www.example.com' }}>Link button</Button>
<Button as={SomeButton}>Button with .btn class</Button>
Configuration of default attributes via attrs
function
type TextFieldProps = {
value: string;
onInput: (e: SyntheticEvent<InputEvent, HTMLInputElement>) => void;
};
const TextField = styled.input.attrs(p => ({ ...p, type: 'text' }))<TextFieldProps>`
border: 4px solid blue;
`;
const PasswordField = styled(TextField).attrs(p => ({ ...p, type: 'password' }))``;
<TextField value={value} onInput={handleInput} />
<PasswordField value={value} onInput={handleInput} />
Style nesting
To describe nested style expressions, you need to use the &
symbol, which, after parsing and generating classes, is replaced with the name of the class of this style.
const Thing = styled.div`
color: blue;
&:hover {
color: red; // <Thing> when hovered
}
& ~ & {
background: tomato; // <Thing> as a sibling of <Thing>, but maybe not directly next to it
}
& + & {
background: lime; // <Thing> next to <Thing>
}
&.something {
background: orange; // <Thing> tagged with an additional CSS class ".something"
}
.something-else & {
border: 1px solid; // <Thing> inside another element labeled ".something-else"
}
`;
<>
<Thing>Hello world!</Thing>
<Thing>How ya doing?</Thing>
<Thing className='something'>The sun is shining...</Thing>
<div>Pretty nice day today.</div>
<Thing>Don't you think?</Thing>
<div className='something-else'>
<Thing>Splendid.</Thing>
</div>
</>
Media and Container queries
The lib allows you to write nested media query and container query expressions as if you were using a CSS preprocessor. Under the hood, expressions are transformed into global expressions in which classes with styles are placed.
const Layout = styled.div`
width: 100%;
background-color: blue;
color: #fff;
@media (max-width: 600px) {
background-color: green;
& span {
color: red;
}
}
`;
<Layout>
This is <span>Content</span>
</Layout>
const Layout = styled.div`
width: 100%;
container: main / inline-size;
& span {
font-size: 2rem;
}
@container main (max-width: 600px) {
& span {
font-size: 1rem;
}
}
`;
<Layout>
This is <span>Content</span>
</Layout>
Referring to other components
const Link = styled.a`
display: inline-flex;
align-items: center;
color: #bf4f74;
padding: 8px;
`;
const Icon = styled.svg`
flex: none;
transition: fill 0.25s;
width: 16px;
height: 16px;
${Link} & {
margin-right: 8px;
}
${Link}:hover & {
fill: blueviolet;
}
`;
<Link href='#'>
<Icon viewBox='0 0 20 20'>
<path d='M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z' />
</Icon>
Some link
</Link>
Render function
This approach can be used to isolate styles without creating styled components for all nested elements.
const Root = styled.main`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 50px minmax(50px, 1fr) 50px;
height: 100vh;
&_header {
background-color: deepskyblue;
border: 1px solid #fff;
}
&_body {
background-color: limegreen;
border: 1px solid #fff;
}
&_footer {
background-color: salmon;
border: 1px solid #fff;
}
`;
<Root>
{fn => (
<>
<div class={fn('header')} />
<div class={fn('body')} />
<div class={fn('footer')} />
</>
)}
</Root>
Animations
The styled fully supports CSS animations. To create an animation you need to use the special keyframes
function.
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 2rem;
`;
<Rotate>🍉</Rotate>
If you want to generate animation based on props, you can represent the animation as a function.
const color = (from: string, to: string) => keyframes`
0% {
background-color: ${from};
}
50% {
background-color: ${to};
}
100% {
background-color: ${from};
}
`;
const Colored = styled.div<{ $from: string; $to: string } & DarkJSX.Elements['div']>`
display: inline-block;
animation: ${p => color(p.$from, p.$to)} 3s linear infinite;
padding: 2rem 1rem;
font-size: 2rem;
`;
<Colored $from='yellow' $to='red'>🍊</Colored>
<Colored $from='green' $to='blue'>🍋</Colored>
Global styles
Typically, styled components are auto-scoped to a local CSS class, providing isolation from other components. However, with createGlobalStyle
, this restriction is lifted, enabling the application of CSS resets or base stylesheets.
const GlobalStyle = createGlobalStyle<{ $light?: boolean }>`
body {
background-color: ${p => (p.$light ? 'white' : 'black')};
color: ${p => (p.$light ? 'black' : 'white')};
}
`;
<GlobalStyle $light />
Theming
The styled offers complete theming support by exporting a <ThemeProvider>
wrapper component. This component supplies a theme to all its child components through the Context API
. Consequently, all styled components in the render tree, regardless of their depth, can access the provided theme.
type Theme = {
accent: string;
};
declare module '@dark-engine/styled' {
export interface DefaultTheme extends Theme {}
}
const Box = styled.div`
width: 100px;
height: 100px;
background-color: ${p => p.theme.accent};
`;
const App = component(() => {
const [theme, setTheme] = useState<Theme>({ accent: '#03A9F4' });
return (
<ThemeProvider theme={theme}>
<Box />
</ThemeProvider>
);
});
The library also provides a useTheme
hook to access the current theme.
Setting inline styles via useStyle
hook
const style = useStyle(styled => ({
root: styled`
width: 100px;
height: 100px;
background-color: darkcyan;
`,
}));
<div style={style.root}></div>
Server Side Rendering
The styled supports server-side rendering, complemented by stylesheet rehydration. Essentially, each time your application is rendered on the server, a ServerStyleSheet
can be created and a provider can be added to your component tree, which accepts styles through a Context API
. Please note that sheet.collectStyles()
already contains the provider and you do not need to do anything additional.
Rendering to string
import { ServerStyleSheet } from '@dark-engine/styled/server';
const sheet = new ServerStyleSheet();
try {
const app = await renderToString(sheet.collectStyles(<App />));
const tags = sheet.getStyleTags();
const mark = '__styled__' // somewhere in your <head></head>
const page = `<!DOCTYPE html>${app}`.replace(mark, tags);
res.statusCode = 200;
res.send(page);
} catch (error) {
console.error(error);
} finally {
sheet.seal();
}
Rendering to stream
const sheet = new ServerStyleSheet();
const stream = sheet.interleaveWithStream(renderToStream(sheet.collectStyles(<App />)));
res.statusCode = 200;
stream.pipe(res);
LICENSE
MIT © Alex Plex