dynamic-styles
v0.0.9
Published
Dynamic Css-in-Js styles engine, based on Emotion
Downloads
2
Maintainers
Readme
🏃♀ Dynamic Styles
Create dynamic stylesheets
and link them to functional components using the React hook
pattern.
- ✅ Build on top of
@emotion/react
: As fast and lightweight as emotion - ✅ Supports all emotion features: lazy evaluation, dynamic theming, etc.
- ✅ Fully featured TypeScript support
- ✅ Server side rendering support: Next.js, Gatsby or any other environment
- ✅
@emotion
cache support - 🟨 Well tested (working on it)
// Create dynamic stylesheet that has access to the previously specified theme and parameters
const useStyles = styleSheet.create(({theme, params}) => ({
root: /* Dynamic Styles */,
button: /* Dynamic Styles */,
text: /* Dynamic Styles */,
}));
const MyComponent = (props) => {
// Access dynamic styles as class names using the created 'useStyles()' hook
// and specify the corresponding parameters
const { classes } = useStyles({ color: props.color, fontSize: 10 });
return (
<div className={classes.root}>
{/* */}
</div>
);
}
💻 Installation
$ yarn add dynamic-styles @emotion/react
# or
$ npm install dynamic-styles @emotion/react
⛳️ Code Sandbox
- React Javascript
- React Typescript
- [⚠️ Not supported, yet] React-Native Javascript
🪁 Basic usage
📂 ./styles.js
To create any styles, we must first instantiate a top-level StyleSheet
instance.
This StyleSheet
instance will be used to create dynamic and reusable stylesheets later.
In the configuration object that the createStylesheet()
method takes up,
we can specify our application's current theme.
We can easily access this theme in the stylesheets we create later.
import { createStyleSheet } from 'dynamic-styles';
// Initialization of a StyleSheet instance called 'styleSheet'
export const styleSheet = createStyleSheet({
theme: {
primaryColor: "#aa11ee",
backgroundColor: "#f3f6f4"
}
});
📂 ./Demo.jsx
In our React Component (MyComponent.jsx
) we can now use the instantiated top-level StyleSheet
instance
to create a dynamic stylesheet for the Component.
Such a dynamic stylesheet can contain multiple styles
clustered in understandable chunks for the different parts of our Component.
For example, styles for the root
container
and some text
contained in the Component.
import React from "react";
import { css } from "@emotion/react";
import { styleSheet } from "./styles";
// Specify dynamic styles and access them later in any React Component
// with the returned 'useStyles()' hook.
const useStyles = styleSheet.create(
({theme, params}) => ({
// Styles of the specified selectors can be created using a css object, ..
root: {
backgroundColor: params.color,
"&:hover": {
backgroundColor: theme.primaryColor,
},
},
// .. or the common 'css()' method provided by '@emotion/react'
text: css`
font-weight: bold;
font-size: ${params.fontSize}px;
color: black;
margin: 0;
`
}),
);
We use the useStyles()
hook, to access the specified styles in the corresponding Component (Demo
)
and feed it with dynamic parameters (params
).
const Demo = (props) => {
const { className } = props;
const [color, setColor] = React.useState("yellow");
// Use the created 'useStyles()' hook to access the specified styles as class names
// and some utility functions like 'cx()' for merging class names.
const { classes, cx } = useStyles({ color, fontSize: 30 });
return (
<div className={cx(classes.root, className)}>
<p className={classes.text}>hello world</p>
<input value={color} onChange={(e) => setColor(e.target.value)} />
</div>
);
}
🔗 Classes merging with cx()
To merge class names, we should use the cx()
method returned by useStyles()
.
It has the same API as the popular clsx package
but is optimized for the use with emotion
.
The key advantage of cx()
is that it detects emotion generated class names
ensuring styles are overwritten in the correct order.
Emotion-generated styles are applied from left to right.
Subsequent styles overwrite property values of previous styles.
import React from "react";
import { styleSheet } from "./styles";
const useStyles = styleSheet.create(({theme}) => ({
button: {
backgroundColor: "gray",
border: 0,
color: "black",
borderRadius: 5,
padding: "10px 20px",
margin: 5,
cursor: "pointer"
},
highlight: {
backgroundColor: theme.primaryColor,
color: "black",
padding: 20
},
bold: {
fontWeight: 1000,
textDecoration: "underline"
}
}));
const Demo = () => {
const [active, setActive] = React.useState(0);
const { classes, cx } = useStyles();
return (
<div>
<button
// Merge styles (class names) using the 'cx()' method
className={cx(classes.button, { [classes.highlight]: active === 0 })}
onClick={() => setActive(0)}
>
First
</button>
<button
// Merge styles (class names) using the 'cx()' method
className={cx(classes.button, classes.bold, { [classes.highlight]: active === 1 })}
onClick={() => setActive(1)}
>
Second (Bold)
</button>
</div>
);
}
🟦 Typescript
The dynamic-styles
API is fully type-safe.
Let's take a look at the Basic usage example converted to Typescript (see below).
The only part worth mentioning that has changed compared to the Javascript example
is that we put withParams()
in front of the create()
method.
This is necessary to tell the create()
method the desired type (e.g. DemoStyles
) of the params
property.
In case you are wondering why we need to go this extra step,
and use withParams<ParamsType>()
to specify the params
generic.
Instead of just specifying the generic
in the create()
method like create<ParamsType>()
.
Well, that's because partial type inference
is not possible in Typescript.
If we were to specify the params
generic in the create()
method (like create<ParamsType>()
)
which, by the way, is possible
we would lose the type inference of the stylesheet object.
Thus, we would have to specify it manually (e.g. create<ParamsType, StyleSheetType>()
).
import React from "react";
import { css } from "@emotion/react";
import { StyleItem } from "dynamic-styles";
import { styleSheet } from "./styles";
type DemoStyles = {
color: string;
fontSize: number;
}
type DemoStyleSheet = {
root: StyleItem;
text: StyleItem;
button: StyleItem;
}
const useStyles = styleSheet.create<DemoStyles, DemoStyleSheet>(
({theme, params}) => ({
root: {
backgroundColor: params.color,
"&:hover": {
backgroundColor: theme.primaryColor,
},
},
text: /* More Styles */,
button: /* More Styles */
}),
);
import React from "react";
import { css } from "@emotion/react";
import { styleSheet } from "./styles";
type DemoStyles = {
color: string;
fontSize: number;
}
// Specify dynamic styles and access them later in any React Component
// with the returned 'useStyles()' hook.
const useStyles = styleSheet
.withParams<DemoStyles>() // <- CHANGE | Specify the 'params' type as generic
.create(
({theme, params}) => ({
// Styles of the specified selectors can be created using a css object, ..
root: {
backgroundColor: params.color,
"&:hover": {
backgroundColor: theme.primaryColor,
},
},
// .. or the common 'css()' method provided by '@emotion/react'
text: css`
font-weight: bold;
font-size: ${params.fontSize}px;
color: black;
margin: 0;
`
}),
);
In the actual Component where we include the created stylesheet with the useStyles()
hook,
we don't need to make any adjustments to achieve full type safety.
const Demo: React.FC<DemoProps> = (props) => {
const { className } = props;
const [color, setColor] = React.useState("yellow");
// Use the created 'useStyles()' hook to access the specified styles as class names
// and some utility functions like 'cx()' for merging class names.
const { classes, cx } = useStyles({ color, fontSize: 30 });
return (
<div className={cx(classes.root, className)}>
<p className={classes.text}>hello world</p>
<input value={color} onChange={(e) => setColor(e.target.value)} />
</div>
);
}
⚗️ Composition and nested selectors
To use a selector (e.g. button
styles) in other parts of the stylesheet,
we need to create a reference to it.
This is necessary because the created useStyles()
hook uses scoped class names
to represent the specified stylesheet.
An established reference created to a selector (e.g. prefix-ref_button_1
), however, remains static.
In order to create such a reference, we can use the createRef()
method,
which is given to the create()
method.
import React from "react";
import { styleSheet } from "./styles";
const useStyles = styleSheet.create(({theme, params, createRef, assignRef}) => {
// Create reference for future use
const button = createRef('button'); // Returns a static selector (e.g. 'prefix-ref_button_1')
return {
// Assign ref variant 1:
button: {
// Assign the reference to the selector via the 'ref' property
ref: button,
// and add any other style properties
backgroundColor: theme.primaryColor,
border: 0,
color: "black",
padding: `10px 20px`,
borderRadius: 5,
cursor: 'pointer',
},
// Assign ref variant 2:
// Assign the reference to the selector via the 'assignRef()' method
button2: assignRef(button, {
// and add any other style properties
backgroundColor: theme.primaryColor,
border: 0,
color: "black",
padding: `10px 20px`,
borderRadius: 5,
cursor: 'pointer',
}),
container: {
display: 'flex',
justifyContent: 'center',
backgroundColor: theme.backgroundColor,
padding: 50,
// Reference button with the previously created static selector
[`&:hover .${button}`]: {
backgroundColor: "rred",
},
},
};
});
const Demo = () => {
const { classes } = useStyles();
return (
<div className={classes.container}>
<button className={classes.button} type="button">
Hover container to change button color
</button>
</div>
);
}
🎥 Keyframes
We can define animations using the keyframes
helper from @emotion/react
.
keyframes
takes in a css keyframe definition
and returns an object we can use in the corresponding styles.
We can use strings or objects just like css
to create such css keyframes.
import React from "react";
import { keyframes } from "@emotion/react";
import { styleSheet } from "./styles";
// Define keyframes with the 'keyframes()' method from '@emotion/react'
const bounce = keyframes`
from, 20%, 53%, 80%, to {
transform: translate3d(0, 0, 0);
}
40%, 43% {
transform: translate3d(0, 30px, 0);
}
70% {
transform: translate3d(0, 15px, 0);
}
90% {
transform: translate3d(0, 4px,0);
}
`
const useStyles = styleSheet.create({
container: {
textAlign: 'center',
// Use created 'bounce' keyframes in the 'container' styles
animation: `${bounce} 3s ease-in-out infinite`,
}
});
const Demo = () => {
const { classes } = useStyles();
return <div className={classes.container}>Keyframes demo</div>;
}
🌍 Global styles
Sometimes we might want to insert global css
styles.
We can use the <GlobalStyles />
component to do this.
import React from "react";
import { GlobalStyles } from "dynamic-styles";
import { useTheme } from "./useTheme";
const App = () => {
const theme = useTheme();
return (
<>
{/* Specify global Styles at the root of your App */}
<GlobalStyles
styles={{
'*, *::before, *::after': {
boxSizing: 'border-box',
},
body: {
backgroundColor: theme.colorScheme === 'dark' ? theme.black : theme.white,
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
lineHeight: 20,
},
}}
/>
{/* The actual App */}
<YourApp />
</>
);
}
🌈 normalize.css
In a web environment it is often necessary to 'normalize' the css
,
which makes the browsers render all elements more consistently
and in line with modern standards.
The NormalizeCss
Component sets the normalized styles
specified in normalize.css globally.
import { NormalizeCSS } from "dynamic-styles";
const App = () => {
return (
<>
<NormalizeCSS />
<YourApp />
</>
);
}
✍️ Inline styles
Often we need to create reusable Components
that should be customizable later on, among other things with inline styles.
We can easily make Components customizable with inline styles
by specifying the styles
property (e.g. partial of specified stylesheet)
in the useStyles()
hook's configuration object.
./components/Button.tsx
Here we create a reusable Button that can be styled
via inline styles using the styles
property.
import React from "react";
import { UseStylesExtractStylesType } from "dynamic-styles";
import { styleSheet } from "../styles";
const useStyles = styleSheet
.withParams<ButtonStyles>()
.create(({theme, params: { color, radius }}) => ({
root: {
color: theme.colors.white,
backgroundColor: color,
borderRadius: radius,
padding: '10px 20px',
cursor: 'pointer',
},
}));
type ButtonStyles = {
color: string;
radius: number;
};
// Create type that represents the created stylesheet type (extracted from the 'useStyles()' hook).
// This type can be used to add a typesafe 'styles' property to the Button component.
export type ExtractedStylesType = UseStylesExtractStylesType<typeof useStyles>;
export const Button: React.FC<ButtonProps> = (props) => {
const { color = 'blue', radius = 0, styles = {}, onClick } = props;
// Pass the 'styles' property to the 'useStyles()' hook
const { classes } = useStyles({ color, radius }, { styles, name: 'Button' });
return (
<button type="button" className={classes.root} onClick={onClick}>
{color} button with {radius}px radius
</button>
);
};
type ButtonProps = {
color?: string;
radius?: number;
styles?: ExtractedStylesType; // Specify the 'styles' prop with full type safety based on the created stylesheet
onClick: () => void;
};
./Demo.tsx
Use the created Button
Component and specify inline
styles
with the styles
property.
import React from "react";
import { css } from "@emotion/react";
import { Button } from "./components/Button";
const Demo: React.FC = () => {
const [toggled, setToggled] = React.useState(false);
return (
<div>
<Button
onClick={() => setToggled(!toggled)}
// Inline styles using the 'styles' property
styles={{
root: css`
background: ${toggled ? "green" : "gray"};
font-weight: bold;
border-radius: 50px;
`,
}}
/>
</div>
);
};
🔨 API documentation
coming soon
❓ FAQ
React-Native StyleSheet
vs dynamic-styles
| | dynamic-styles
| React-Native
Stylesheet |
|-----------------------------------------------|------------------|---------------------------|
| Compatible with React-Native
| ✅ | ✅ |
| Compatible with React
| ✅ | ❌ |
| Access global theme | ✅ | 🟨 |
| Influence styles via props
of the Component | ✅ | ❌ |
| Styling with JavaScript
Object | ✅ | ✅ |
| Styling with Emotion
styles | ✅ | ❌ |
Why dynamic-styles
and not just using tss-react
?
Because tss-react
was explicitly designed as a replacement for the makeStyle()
API
deprecated in Material UI 5 and thus isn't optimized for general use (without Material UI).
Also, did it not meet all my needs, such as creating styles with the css()
method provided by Emotion
and wasn't fully typesafe.
const useStyles = styleSheet.create((theme, params) => ({
root: css`
// Write styles as in CSS and not in the form of an object
`,
}));
🎉 Inspired by:
The syntax of dynamic-styles
is inspired by the React Native Stylesheet API.
Under the hood, we have been partly inspired by TSS-React
and Mantine-Styles.