Provide a theme-like scheme (with design tokens and aliases) to generate CSS custom properties and a JavaScript object to be used as its reference.
- Install themizer via
pnpm
or another preferred package manager.
pnpm add themizer@latest
- Add the script into the
package.json
file to whenever the scheme is ready to start the build process.
"scripts": {
+ "themizer:atoms": "themizer --out-dir=./src/app"
},
Import themizer to generate the atoms, and unwrapAtom along with resolveAtom as helpers to get the custom property or default value of an atom.
import themizer, { unwrapAtom, resolveAtom } from "themizer";
const { aliases, medias, tokens } = themizer({ prefix, medias, tokens }, aliases);
const unwrapedAtom = unwrapAtom(aliases[key] /* or tokens[key] */);
const resolvedAtom = resolveAtom(aliases[key] /* or tokens[key] */)
Property | Description |
---|---|
tokens | Object that writes CSS responsive custom properties to atoms.css file, serves aliases as its function parameter, and theme.tokens as a JavaScript reference |
options.medias | Object with values as media queries and key as its own aliases to replace the responsive properties in the aliases object values. It's also accessed throught theme.medias as it is as a JavaScript reference with the addition of @media * in its values |
options.prefix? | String to prefix all the generated CSS custom properties. It's required to avoid colision with other libraries custom properties |
aliases | Function to generate theme.rules having options.tokens passed as parameter, and theme.aliases as a JavaScript reference |
Property | Type | Description |
---|---|---|
theme.aliases | Object | Object containing the same keys as aliases parameter, but returning its CSS custom properties wrapped within var(*) . Use the utility unwrapAtom to unwrap it |
theme.tokens | Object | Object containing the same keys as options.tokens parameter, but returning its CSS custom properties wrapped within var(*) . Use the utility unwrapAtom to unwrap it |
theme.medias | Object | Object containing the options.medias to be also used as reference in JavaScript |
Type | Description |
---|---|
string | Function to get generated custom properties. I.e. unwrapAtom('var(--property)') returns --property . Useful to change scoped custom properties |
Type | Description |
---|---|
string | String containing the unwrapped custom property |
Type | Description |
---|---|
string | Function to get the default value of a custom propertie. I.e. resolveAtom('var(--property, value)') returns value . Useful for reusability and keeping its source of truth |
Type | Description |
---|---|
string | String containing the default value of a custom property |
When the configuration is done, execute pnpm run themizer
to inject the generated responsive custom properties into the choosen output directory.
Parameter | Description |
---|---|
--watch | Executes the command everytime the file themizer.config.ts is saved |
--out-dir | Injects the generated responsive custom properties into the given file |
- Create a file and provide any tokens and options based on the product's design system needs.
// src/lib/theme.ts
import themizer from 'themizer'
const theme = themizer(
{
// "jv" in this example stands to João Victor, my name
prefix: 'jv',
medias: {
desktop: '(min-width: 1024px)',
// Nothing surprising, queries can be combined
darkNonprint: '(prefers-color-scheme: dark) and not print',
motion: '(prefers-reduced-motion: no-preference)',
},
tokens: {
alphas: {
primary: 0.8,
},
units: {
16: '1rem',
24: '1.5rem',
40: '2.5rem',
}
8000
,
timing: {
bounce: 'cubic-bezier(0.5, -0.5, 0.25, 1.5)',
},
duration: {
fast: '200ms',
},
colors: {
amber: {
50: '#fffbf4',
500: '245 158 11',
950: '#100a01',
},
},
},
},
(tokens) => ({
palette: {
main: {
primary: `rgb(${tokens.colors.amber[500]} / ${tokens.alphas.primary})`,
},
foreground: [{ darkNonprint: tokens.colors.amber[50] }, tokens.colors.amber[950]],
background: [{ darkNonprint: tokens.colors.amber[950] }, tokens.colors.amber[50]],
},
typography: {
heading: [{ desktop: tokens.units[40] }, tokens.units[24]],
body: tokens.units[16],
},
motion: {
bounce: [{ motion: `${tokens.trans.duration.fast} ${tokens.trans.timing.bounce}` }],
},
}),
)
export default theme
Note that
prefix
was specified as "jv". Otherwise, the generated custom properties would start with--tokens-*
for tokens and--aliases-*
for aliases.
The generated custom properties would be written in your product's app src/app/atoms.css
path, and look like this:
/* src/app/atoms.css */
:root {
--jv-tokens-alphas-primary: 0.8;
--jv-tokens-units-16: 1rem;
--jv-tokens-units-24: 1.5rem;
--jv-tokens-units-40: 2.5rem;
--jv-tokens-timing-bounce: cubic-bezier(0.5, -0.5, 0.25, 1.5);
--jv-tokens-duration-fast: 200ms;
--jv-tokens-colors-amber-50: #fffbf4;
--jv-tokens-colors-amber-500: 245 158 11; /* #f59e0b */
--jv-tokens-colors-amber-950: #100a01;
--jv-aliases-palette-main-primary: rgb(var(--jv-tokens-colors-amber-500) / var(--jv-tokens-alphas-primary));
--jv-aliases-palette-foreground: var(--jv-tokens-colors-amber-950);
--jv-aliases-palette-background: var(--jv-tokens-colors-amber-50);
--jv-aliases-typography-heading: var(--jv-tokens-units-24);
--jv-aliases-typography-body: var(--jv-tokens-units-16);
}
@media (prefers-color-scheme: dark) and not print {
:root {
--jv-aliases-palette-foreground: var(--jv-tokens-colors-amber-50);
--jv-aliases-palette-background: var(--jv-tokens-colors-amber-950);
}
}
@media (min-width: 1024px) {
:root {
--jv-aliases-typography-heading: var(--jv-tokens-units-40);
}
}
@media (prefers-reduced-motion: no-preference) {
:root {
--jv-aliases-motion-bounce: var(--jv-tokens-duration-fast) var(--jv-tokens-timing-bounce);
}
}
- Index the generated CSS in the project's top-level file, and also start using themizer's helpers.
// Example of src/app/layout.tsx Next.js file.
import { type Viewport } from 'next'
+ import { resolveAtom } from 'themizer'
+ import theme from '@/lib/theme'
import './global.css'
+ import './atoms.css'
export const viewport: Viewport = {
themeColor: [
{
media: '(prefers-color-scheme: dark)',
+ color: `rgb(${resolveAtom(theme.tokens.colors.amber[950])})`,
- color: '#100a01',
},
{
media: '(prefers-color-scheme: light)',
+ color: `rgb(${resolveAtom(theme.tokens.colors.amber[50])})`,
- color: '#fffbf4',
},
],
}
export default function AppLayout({ children }: React.PropsWithChildren) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
)
}
Customize Tailwind CSS' default theme with the generated by themizer
.
// tailwind.config.ts
import { type Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import { type CSSRuleObject } from 'tailwindcss/types/config';
+ import theme from './lib/theme';
export default {
content: ['./app/**/*.{tsx,ts,mdx}', './components/**/*.{tsx,ts,mdx}'],
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
+ background: theme.aliases.palette.background,
+ foreground: theme.aliases.palette.foreground,
},
fontSize: {
+ heading: [theme.aliases.typography.heading, { lineHeight: theme.tokens.units[24] }],
+ body: [theme.aliases.typography.body],
},
},
} satisfies Config
Implement React components using the generated theme.
// src/components/Heading.tsx
import { unwrapAtom } from 'themizer'
export default function Heading({ className = '', ...props }: React.ComponentProps<'h1'>) {
return (
<h1
className={`text-heading ${className}`}
{...props}
styles={{
/* If this component has to have it's font-size locked for some reason */
[unwrapAtom(theme.aliases.typography.heading)]: theme.tokens.units[24],
}}
/>
)
}
Apply the class names normally to style anything. And don't worry, intellisense will charmingly work.
// src/components/Title.tsx
import theme from '@/lib/theme'
export default function Title({ children, className = '', ...props }: React.ComponentProps<'h1'>) {
return (
<h1
className={`heading ${className}`}
{...props}>
{children}
<style jsx>{`
.heading {
font-size: ${theme.aliases.typography.heading};
}
`}</style>
</h1>
)
}
Remember to create a styled-jsx registry to collect all CSS rules in a render and inject the theme rules in the document.