Generate CSS3 custom properties based on a given theme with tokens and aliases serving as a reference.
const { aliases, medias, tokens, rules } = getTheme(aliases, options?);
-
aliases
Object for basic
theme.rules
generation, andtheme.aliases
as a JavaScript reference.It's a function if
options.tokens
is provided, being passed asaliases
parameter. -
options?
-
prefixVars?
String to prefix all the generated CSS custom properties, accessed in
theme.rules
. -
medias?
Object with values as media queries and properties serving as its aliases.
It's used to replace the responsive properties in the
aliases
object values.It's also accessed throught
theme.medias
as it is. -
tokens?
Object for advanced
theme.aliases
as function.It's also generates variables that can be referenced by
theme.tokens
in JavaScript.
-
-
theme
-
aliases
Object containing the same keys as
aliases
parameter, but returning its CSS custom properties. -
medias?
Object containing the
options.medias
to be also used as reference in JavaScript. -
tokens?
Object containing the same keys as
options.tokens
parameter, but returning its CSS custom properties. -
rules
Object containing style sheets the generated custom properties from
aliases
andoptions.tokens
.-
css
String containing the custom properties as CSS format.
-
jss
Object containing the custom properties as JSS format.
Needs to be injected at the top-level page application.
-
-
Create a file and provide any tokens and options based on the product's design system needs.
// helpers/rgba.ts
export default function rgba(color: string, alpha: string) {
return `color-mix(in srgb, ${color} calc(${alpha} * 100), transparent)`;
}
// src/lib/theme.ts
import { getTheme } from 'ui-tokens';
import rgba from 'helpers/rgba';
const theme = getTheme(
(tokens) => ({
palette: {
main: tokens.colors.amber[500],
accent: [{ dark: tokens.colors.amber[400] }, tokens.colors.amber[600]],
text: {
primary: [{ dark: tokens.colors.white }, rgba(tokens.colors.black, tokens.alphas.primary)],
secondary: [{ dark: rgba(tokens.colors.white, tokens.alphas.secondary) }, rgba(tokens.colors.black, tokens.alphas.secondary)],
},
},
grid: {
margin: [{ desktop: tokens.dimensions[40] }, tokens.dimensions[16]],
gutter: tokens.dimensions[8],
},
typography: {
headline: [{ desktop: tokens.dimensions[64] }, tokens.dimensions[40]],
body: tokens.dimensions[16],
},
motion: {
bounce: [{ motion: `${tokens.trans.duration.fast} ${tokens.trans.timing.bounce}` }],
ease: [{ motion: `${tokens.trans.duration.fast} ${tokens.trans.timing.ease}` }],
},
}),
{
prefixVars: 'ds',
medias: {
desktop: '@media (min-width: 1024px)',
dark: '@media (prefers-color-scheme: dark)',
motion: '@media (prefers-reduced-motion: no-preference)',
},
tokens: {
colors: {
amber: {
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
},
white: '#fff',
black: '#000',
},
alphas: {
primary: 0.8,
secondary: 0.6,
},
dimensions: {
8: '0.5rem',
16: '1rem',
40: '2.5rem',
64: '4rem',
},
font: {
sans: 'sofia-pro',
weight: {
regular: 400,
semibold: 600,
bold: 800,
},
},
trans: {
timing: {
bounce: 'cubic-bezier(0.5, -0.5, 0.25, 1.5)',
ease: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
},
duration: {
fast: '200ms',
},
},
},
},
);
The generated custom properties from the last example would look like this.
Note that the provided
prefixVars
("ds") is being used. Otherwise, these custom properties would be just like--tokens-*
for tokens and--aliases-*
for aliases.
Generated styles within theme.rules.css
:
:root {
/* Generated tokens */
--ds-tokens-colors-amber-400: #fbbf24;
--ds-tokens-colors-amber-500: #f59e0b;
--ds-tokens-colors-amber-600: #d97706;
--ds-tokens-colors-white: #fff;
--ds-tokens-colors-black: #000;
--ds-tokens-alphas-primary: 0.8;
--ds-tokens-alphas-secondary: 0.6;
--ds-tokens-dimensions-8: 0.5rem;
--ds-tokens-dimensions-16: 1rem;
--ds-tokens-dimensions-40: 2.5rem;
--ds-tokens-font-sans: 'sofia-pro';
--ds-tokens-font-weight-regular: 400;
--ds-tokens-font-weight-semibold: 600;
--ds-tokens-font-weight-bold: 800;
--ds-tokens-trans-timing-bounce: cubic-bezier(0.5, -0.5, 0.25, 1.5);
--ds-tokens-trans-duration-fast: 200ms;
/* Generated aliases */
--ds-aliases-palette-main: var(--ds-tokens-colors-amber-500);
--ds-aliases-palette-accent: var(--ds-tokens-colors-amber-600);
--ds-aliases-palette-text-primary:
color-mix(
in srgb,
var(--ds-tokens-colors-black) calc(var(--ds-tokens-alphas-primary) * 100),
transparent
);
--ds-aliases-palette-text-secondary:
color-mix(
in srgb,
var(--ds-tokens-colors-black) calc(var(--ds-tokens-alphas-secondary) * 100),
transparent
);
--ds-aliases-grid-margin: var(--ds-tokens-dimensions-16);
--ds-aliases-grid-gutter: var(--ds-tokens-dimensions-8);
--ds-aliases-typography-body: var(--ds-tokens-dimensions-16);
--ds-aliases-typography-headline: var(--ds-tokens-dimensions-40);
}
@media (prefers-color-aliases: dark) {
:root {
--ds-aliases-palette-accent: var(--ds-tokens-colors-amber-400);
--ds-aliases-palette-text-primary: var(--ds-tokens-colors-white);
--ds-aliases-palette-text-secondary:
color-mix(
in srgb,
var(--ds-tokens-colors-white) calc(var(--ds-tokens-alphas-secondary) * 100),
transparent
);
}
}
@media (min-width: 1024px) {
:root {
--ds-aliases-grid-margin: var(--ds-tokens-dimensions-40);
--ds-aliases-typography-headline: var(--ds-tokens-dimensions-64);
}
}
@media (prefers-reduced-motion: no-preference) {
:root {
--ds-aliases-motion-bounce:
var(--ds-tokens-trans-duration-fast)
var(--ds-tokens-trans-timing-bounce);
}
}
Generated styles within theme.rules.jss
:
console.log(theme.rules.jss);
/*
{
':root': {
'--ds-tokens-colors-amber-400': '#fbbf24',
'--ds-tokens-colors-amber-500': '#f59e0b',
'--ds-tokens-colors-amber-600': '#d97706',
'--ds-tokens-colors-white': '#fff',
'--ds-tokens-colors-black': '#000',
'--ds-tokens-alphas-primary': 0.8,
'--ds-tokens-alphas-secondary': 0.6,
'--ds-tokens-dimensions-8': '0.5rem',
'--ds-tokens-dimensions-16': '1rem',
'--ds-tokens-dimensions-40': '2.5rem',
'--ds-tokens-font-sans': 'sofia-pro',
'--ds-tokens-font-weight-regular': 400,
'--ds-tokens-font-weight-semibold': 600,
'--ds-tokens-font-weight-bold': 800,
'--ds-tokens-trans-timing-bounce': 'cubic-bezier(0.5, -0.5, 0.25, 1.5)',
'--ds-tokens-trans-duration-fast': '200ms',
'--ds-aliases-palette-main': 'var(--ds-tokens-colors-amber-500)',
'--ds-aliases-palette-accent': 'var(--ds-tokens-colors-amber-600)',
'--ds-aliases-palette-text-primary': 'color-mix(in srgb, var(--ds-tokens-colors-black) calc(var(--ds-tokens-alphas-primary) * 100), transparent)',
'--ds-aliases-palette-text-secondary': 'color-mix(in srgb, var(--ds-tokens-colors-black) calc(var(--ds-tokens-alphas-secondary) * 100), transparent)',
'--ds-aliases-grid-margin': 'var(--ds-tokens-dimensions-16)',
'--ds-aliases-grid-gutter': 'var(--ds-tokens-dimensions-8)',
'--ds-aliases-typography-body': 'var(--ds-tokens-dimensions-16)',
'--ds-aliases-typography-headline': 'var(--ds-tokens-dimensions-40)',
},
'@media (prefers-color-aliases: dark)': {
':root': {
'--ds-aliases-palette-accent': 'var(--ds-tokens-colors-amber-400)',
'--ds-aliases-palette-text-primary': 'var(--ds-tokens-colors-white)',
'--ds-aliases-palette-text-secondary': 'color-mix(in srgb, var(--ds-tokens-colors-white) calc(var(--ds-tokens-alphas-secondary) * 100), transparent)',
},
},
'@media (min-width: 1024px)': {
':root': {
'--ds-aliases-grid-margin': 'var(--ds-tokens-dimensions-40)',
'--ds-aliases-typography-headline': 'var(--ds-tokens-dimensions-64)',
},
},
'@media (prefers-reduced-motion: no-preference)': {
':root': {
'--ds-aliases-motion-bounce': 'var(--ds-tokens-trans-duration-fast) var(--ds-tokens-trans-timing-bounce)',
}
},
}
*/
The generated CSS custom properties needs to be indexed at the project's top-level file.
- Create a styled-jsx registry to collect all CSS rules in a render and inject the theme rules in the document.
// src/providers/StyledJsxProvider.tsx
'use client';
import { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { StyleRegistry, createStyleRegistry } from 'styled-jsx';
import theme from 'lib/theme';
export default function StyledJsxProvider({
children,
}: React.PropsWithChildren) {
const [styleRegistry] = useState(() => createStyleRegistry());
useServerInsertedHTML(() => {
const styles = styleRegistry.styles();
styleRegistry.flush();
return <>{styles}</>;
});
return (
<StyleRegistry registry={styleRegistry}>
<style jsx global>{`
${theme.rules}
/* Other CSS styles, such as Reset, Normalize and so on. */
`}</style>
{children}
</StyleRegistry>
);
}
- Wrap the children of the root layout with the style registry component.
// src/app/layout.tsx
import type { Viewport } from 'next';
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import StyledJsxProvider from 'providers/StyledJsxProvider';
// There are also cool helper functions to get a custom property name or value.
import { resolveVar, unwrapVar } from 'ui-tokens';
export const viewport: Viewport = {
themeColor: [
{
media: '(prefers-color-scheme: dark)',
color: resolveVar(theme.tokens.colors.amber[400]), // #fbbf24
},
{
media: '(prefers-color-scheme: light)',
color: resolveVar(theme.tokens.colors.amber[600]), // #d97706
},
],
};
export default function RootLayout({ children }: React.PropsWithChildren) {
return (
<html
lang="en"
style={{
[unwrapVar(trans.duration.fast) /* --ds-tokens-trans-duration-fast */]: '400ms',
}}>
<body>
<StyledJsxProvider>
{children}
<Analytics />
<SpeedInsights />
</StyledJsxProvider>
</body>
</html>
);
}
- Implement components using the generated theme.
// src/components/Title.tsx
'use client';
import theme from './lib/theme';
export default function Title({
children,
className = '',
...props
}: React.ComponentPropsWithoutRef<'h1'>) {
return (
<h1 className={`title ${className}`} {...props}>
{children}
<style jsx>{`
.title {
font-size: ${theme.aliases.typography.headline}; /* var(--ds-aliases-typography-headline, var(--ds-tokens-dimensions-40, 2.5rem)) */
font-weight: ${theme.tokens.font.weight.bold}; /* var(--ds-tokens-font-weight-bold, 800) */
}
`}</style>
</h1>
);
}
- Or anywhere else it's needed.
No need to change de directive of a file to use theme.
// src/app/page.tsx
import Title from 'components/Title';
import theme from 'lib/theme';
export default function Page() {
return (
<main>
<Title>Styled with Styled JSX</Title>
</main>
);
}
Soon!