Fully configurable framework-agnostic theme system (spec, theme, renderer, themed styles/keyframes/CSS variables) for building UIs.
Your theme your rules 🤘.
uinix-theme provides a small set of APIs to build and maintain theme systems. Key feature highlights:
- Framework-agnostic (works with any view library)
- Build-free (works directly in browsers)
- Fully configurable (anything is themable based on your spec)
- Themed styles (all CSS properties including
filters
,animations
) - Themed CSS keyframes
- Themed CSS variables
- Modern CSS-in-JS features
- Responsive styles
- Atomic CSS
To further explore uinix-theme, visit the Theme Playground for interactive demos, or read the guides at the official documentation website.
This package is ESM-only.
Install in Node 12+ with npm:
npm install uinix-theme
import {...} from 'https://esm.sh/uinix-theme';
Install in browsers with esm.sh:
<script type="module">
import {...} from 'https://esm.sh/uinix-theme';
</script>
For a concise and interactive exploration of uinix-theme, please visit the Theme Playground or read the guides at the official documentation website.
The following sections provide a comprehensive overview of using uinix-theme. Please refer to the § Glossary for definitions of italicized terms referenced throughout this document.
A theme spec is an object relating theme properties (keys) and CSS properties (values). It is used as a specification together with a theme to inform how themed styles should be resolved and rendered to CSS.
Import supported theme specs in the uinix ecosystem with:
import themeSpec from 'uinix-theme-spec';
console.log(themeSpec);
Yields:
const themeSpec = {
'animations': ['animation'],
'backgrounds': ['background'],
...
'spacings': [
'margin',
'marginBlock',
'marginBlockEnd',
'marginBottom',
'padding',
'paddingBottom',
'paddingLeft',
'paddingRight',
'paddingTop',
...
],
...
}
You can create your own theme spec by specifying the relationships of theme properties and CSS properties. This allows you to fully configure and manage your theme spec for your theme system.
const themeSpec = {
colors: [
'backgroundColor',
'borderColor',
'color',
],
// for example, you may want to split "spacings" into two explicit groups instead
margins: [
'margin',
'marginBottom',
'marginLeft',
'marginRight',
'marginTop',
],
paddings: [
'padding',
'paddingBottom',
'paddingLeft',
'paddingRight',
'paddingTop',
],
...
};
A theme is an (optionally recursive) object relating theme properties with CSS values. It provides a way to define and reference CSS values via theme values.
const theme = {
colors: {
brand: { // can be recursively specified
primary: 'red',
link: 'blue',
},
},
paddings: {
s: 4,
m: 8,
l: 16,
},
};
The following theme values (authored as JSONPath syntax relative to the provided theme) resolve to their respective assigned CSS values.
colors.brand.primary
:'red'
colors.brand.link
:'blue'
paddings.s
:4
paddings.m
:8
paddings.l
:16
A theme renderer provides ways to resolve themed styles based on the provided theme and theme spec, and renders the resolved CSS to the DOM.
Create and configure a theme renderer based on the provided theme and theme spec with createThemeRenderer
:
import {createThemeRenderer} from 'uinix-theme';
const renderer = createThemeRenderer({
theme,
themeSpec,
});
Initialize and load the theme renderer in a single entry point in your code to render CSS to the DOM.
renderer.load();
Render a themed style object with renderer.renderStyle
:
const style = {
color: 'brand.primary', // theme values are authored in JSONPath syntax based on their definitions in the theme.
fill: 'rgba(0, 0, 0, 0.5)', // CSS values are also valid
padding: 'm',
':hover': {
'> a': {
color: 'brand.link',
padding: 's',
}
},
};
renderer.renderStyle(style);
Yields the following rendered CSS:
.x {
color: red;
fill: rgba(0, 0, 0, 0.5);
padding: 8px;
}
.x:hover > a {
color: blue;
padding: 4px
}
To express styles as a simple function of state and props, simply pass a style rule (function) to renderer.renderStyle
:
const styleRule = (props) => ({
color: props.isPrimary ? 'brand.primary' : 'black',
padding: 'm',
});
renderer.renderStyle(
styleRule,
{isPrimary: true}, // props for the provided style rule
);
Yields the following rendered CSS:
.x {
color: red;
padding: 8px;
}
Note: Please refer to the examples of style and themed style in the § Glossary for details on authoring CSS-in-JS styles and how the themed styles are resolved by the theme renderer using the provided theme and theme spec.
You can render themed global styles with renderer.renderGlobalStyles
:
const globalStyles = {
body: {
color: 'brand.primary',
padding: 'm',
},
'*': {
boxSizing: 'border-box',
},
'a:hover': {
color: 'brand.link',
padding: 's',
},
};
renderer.renderGlobalStyles(globalStyles);
Yields the following global CSS styles:
body {
color: red;
padding: 8px;
}
* {
box-sizing: border-box;
}
a:hover {
color: blue;
padding: 4px;
}
To render themed CSS keyframes, first ensure that the themeSpec
is configured to register the animationName
CSS property (we recommend using keyframes
as the canonical theme property).
const themeSpec = {
... // same keys/values in earlier examples.
keyframes: ['animationName'],
};
Attach the CSS keyframes in JS object notation under the registered theme property (keyframes
):
const theme = {
keyframes: {
flicker: {
'0%': {opacity: '0'},
'50%': {opacity: '1'},
'100%': {opacity: '0'},
},
spin: {
circle: { // can be recursively specified
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(360deg)',
},
},
},
},
};
We can now render and resolve themed CSS keyframes using appropriate CSS animation
short-hand techniques:
const style = {
animation: '1s linear infinite', // CSS "animation" shorthand
animationName: 'spin.circle', // overwrite the "animationName" CSS property with a theme value
};
renderer.renderStyle(style);
Yields the following CSS:
.x {
animation: 1s linear infinite;
animation-name: k1; /* generated CSS keyframe based on the what is specified in theme.keyframes */
}
If you would like the renderer to render themed styles as atomic CSS, configure this in createThemeRenderer
:
import {createThemeRenderer} from 'uinix-theme';
const renderer = createThemeRenderer({
enableAtomicCss: true,
theme,
themeSpec,
});
renderer.load();
const style1 = {
color: 'brand.primary';
padding: 'm',
};
const style2 = {
color: 'brand.primary';
padding: 'l',
};
renderer.renderStyle(style1);
renderer.renderStyle(style2);
Yields the following rendered atomic CSS classes:
/* Every CSS property/value pair is generated as a unique CSS class. */
.x {
color: red;
}
.y {
padding: 8px;
}
.z {
padding: 16px;
}
Note: We recommend enabling atomic CSS in production as a scalable solution to share and reuse CSS class definitions across HTML elements. Disabling atomic styles in development is recommended to improve development experience.
If you prefer to work with CSS variables and would like to integrate CSS workstreams with uinix-theme, you can configure rendering the entire theme as CSS variables into the global style sheet.
import {createThemeRenderer} from 'uinix-theme';
const renderer = createThemeRenderer({
enableCssVariables: true,
theme,
themeSpec,
});
renderer.load();
const globalStyles = {...}; // your other global styles
renderer.renderGlobalStyles(globalStyles);
Yields the following rendered global CSS:
:root {
--colors-brand-primary: red;
--colors-brand-link: blue;
--paddings-s: 4px;
--paddings-m: 8px;
--paddings-l: 16px;
};
/* your other global styles */
In addition, this feature also attempts to resolve themed styles to use CSS variables whenever possible.
const style = {
backgroundColor: 'purple',
color: 'brand.primary',
margin: 'not.a.valid.theme.value',
padding: 'm',
}
renderer.renderStyle(style);
Yields the following CSS:
.x {
background-color: purple; /* just a CSS value */
color: var(--colors-brand-primary); /** resolves to a themed CSS variable */
padding: var(--paddings-m); /** resolves to a themed CSS variable *./
/* margin is not rendered as it cannot be resolved */
}
Responsive styles are easily supported by configuring the theme renderer appropriately to specify responsive breakpoints and whitelist CSS properties to be responsive-aware:
import {createThemeRenderer} from 'uinix-theme';
const renderer = createThemeRenderer({
responsiveBreakpoints: ['400px', '800px'], // min-width-based
responsiveCssProperties: ['padding', 'margin'],
theme,
themeSpec,
});
Specify responsive styles for the provided breakpoints:
const responsiveStyle = {
color: ['black', 'brand.primary', 'brand.link'],
padding: ['s', 'm', 'l'],
};
Yields the following rendered CSS
.x {
color: black;
padding: 4px;
}
@media (min-width: 400px) {
.x {
padding: 8px;
}
}
@media (min-width: 800px) {
.x {
padding: 16px;
}
}
Note: While
color
was specified inresponsiveStyle
, it is not resolved because it was not explicitly whitelisted inoptions.responsiveCssProperties
.
uinix-theme exports the following identifiers:
combineStyles
createCssVariables
createThemeRenderer
There are no default exports.
APIs are explorable via JSDoc-based Typescript typings accompanying the source code.
Please refer to the § Glossary for definitions of italicized terms referenced throughout this document.
Combines an array of style objects or style rules and returns a single composed style rule.
An array of StyleObject
or StyleRule
.
A single composed style rule.
Example
import {combineRules} from 'uinix-theme';
const styleRule1 = props => ({
fontSize: props.fontSize,
color: 'red',
});
const styleRule2 = props => ({
color: 'blue',
});
const combinedRule = combineRules([styleRule1, styleRule2]);
Effectively behaves as
const combinedStyleRule = props => ({
fontSize: props.fontSize,
color: 'blue',
});
Creates an object of CSS variables using the provided theme. CSS variables are named based on the theme values defined in the provided theme.
See theme defined in § Glossary.
Prepends a namespace prefix to every rendered CSS variable. Namespaces can only consist of a-z0-9-_
(lowercase alphanumerals) and must begin with a-z_
.
See theme spec defined in § Glossary.
By default, one does not typically need to provide a theme spec to create themed CSS variables. However without a theme spec, createCssVariables
will not have enough information to determine a few CSS property assumptions. For example, 'px'
-based CSS properties assigned numeric values may not correctly be resolved to actual px
values. Supply a theme spec in such situations.
An object containing resolved CSS variables.
Example
Given the following theme
and optional namespace
,
import {createCssVariables} from 'uinix-theme';
const theme = {
colors: {
brand: {
primary: 'blue',
},
},
spacings: {
s: 4,
m: 8,
l: 16,
},
};
const themeSpec = {
colors: ['color'],
spacings: ['margin', 'padding'],
};
const cssVariables = createCssVariables(theme, {namespace: 'uinix', themeSpec});
Yields the following CSS variables.
const cssVariables = {
'--uinix-colors-brand-primary': 'blue',
// resolved to px values because a theme-spec is provided
'--uinix-spacings-s': '4px',
'--uinix-spacings-m': '8px',
'--uinix-spacings-l': '16px',
};
Creates a theme renderer to resolve themed styles based on the provided theme and theme spec, and render the resolved styles to the DOM.
Enables rendering styles as atomic CSS.
When enabled, will support CSS variables features in the renderer
methods:
renderer.renderGlobalStyles
will now render thetheme
as CSS variables under the:root
pseudo class.renderer.renderStyle
will now resolve themed styles into its corresponding CSS variables.
Prepends a namespace prefix to every rendered CSS classname, keyframe, and variable. Namespaces can only consist of a-z0-9-_
(lowercase alphanumerals) and must begin with a-z_
.
Configure this to support responsive styles based on the provided breakpoints. Breakpoints are min-width
-based media queries.
Whitelist the corresponding responsive CSS properties to be responsive-aware.
See theme defined in § Glossary.
See theme spec defined in § Glossary.
Returns a theme renderer with methods to resolve and render themed styles to the DOM.
renderer.load()
: Initializes and loads the renderer.renderer.renderStyle(style, props?)
: Resolves and renders the provided style object or style rule). Accepts optional style props.renderer.renderGlobalStyles(style)
: Resolves and renders the provided global styles object.renderer.unload()
: Unloads and removes all rendered CSS.
Example
Create and configure a theme renderer with:
import {createThemeRenderer} from 'uinix-theme';
const theme = {...};
const themeSpec = {...};
const renderer = createThemeRenderer({
enableAtomicCss: true,
enableCssVariables: true,
namespace: 'uinix',
responsiveBreakpoints: ['400px', '800px'],
responsiveCssProperties: ['padding', 'margin'],
theme,
themeSpec,
});
Initialize the renderer in a single entry point in your code with:
renderer.load();
Render global styles with:
const globalStyles = {
'body': {...}
'*': {...},
'.vendor-classname': {...}
};
renderer.renderGlobalStyles(globalStyles);
Render either style objects or style rules with:
const styleObject = {
color: 'brand.primary',
':hover': {
color: 'brand.link',
},
};
const styleRule = (props) => ({
color: 'brand.primary',
padding: props.isPadded ? 'm' : 0,
});
renderer.renderStyle(styleObject);
renderer.renderStyle(styleRule, {isPadded: true});
Unload and clear all rendered styles with:
renderer.unload();
The following are theme-specs usable by uinix-theme.
uinix-theme-spec
— the default uinix-theme spec.uinix-theme-spec-theme-ui
— the theme-ui spec usable by uinix-theme.
Example
import {createThemeRenderer} from 'uinix-theme';
import themeSpec from 'uinix-theme-spec';
const renderer = createThemeRenderer({
theme: {
colors: {
brand: {
primary: 'red',
link: 'blue',
},
},
spacings: {
s: 4,
m: 8,
l: 16,
},
},
themeSpec,
});
renderer.load();
The following terms used throughout this documentation are defined below. We will reference the following example objects throughout this section.
const theme = {
colors: {
brand: {
primary: 'blue',
secondary: 'yellow',
},
},
spacings: {
s: 4,
m: 8,
l: 16,
},
};
const themeSpec = {
colors: [
'backgroundColor',
'color',
],
spacings: [
'margin',
'marginBottom',
'marginLeft',
'marginRight',
'marginTop',
],
};
-
Theme: An (optionally recursive) object relating theme properties with CSS values. Provides a way to define and reference CSS values via theme values.
Example
theme
defined above is an example of a theme. -
Theme property: The keys of a theme that relates to their corresponding CSS property as defined in the theme spec.
Example
The
colors
andspacings
are theme properties based ontheme
. -
Theme value: JSONPath-like syntax to refer to CSS values in a theme.
Example
The
'colors.brand.primary'
and'spacings.m'
theme values would refer to the CSS values'blue'
and8
respectively based on how they are defined intheme
. -
Theme spec: An object relating theme properties with CSS properties. It is used as a specification together with a theme to inform how themed styles should be resolved and rendered to CSS.
Example
Referring to the
theme
andthemeSpec
above, we note that- the
colors
theme property relates to thebackgroundColor
,color
CSS properties. - the
spacings
theme property relates to themargin
,marginBottom
,marginLeft
,marginRight
,marginTop
CSS properties.
- the
-
Themed style: A style that is specified with theme values instead of CSS values (CSS values can still be specified). A themed style is only meaningful in relation to a theme and theme spec, as the following example demonstrates.
Example
Given the following themed style,
const themedStyle = { color: 'brand.primary', margin: 'm', fill: 'red', top: 64, backgroundColor: 'not.a.valid.theme.path', padding: 'm', };
It will resolve to the following CSS based on the
theme
andthemeSpec
..x { color: blue; fill: red; margin: 8px; top: 64, }
color
is resolved to the CSS value'blue'
by checking that thecolor
CSS property is registered inthemeSpec
(under thecolors
theme property) and that the'brand.primary'
theme value is resolvable undertheme.colors
.margin
is resolved to the CSS value'8px'
by checking that themargin
CSS property is registered inthemeSpec
(under thespacings
theme property) and that the'm'
theme value is resolvable undertheme.spacings
.fill
andtop
simply use their specified CSS values as fallback values since they cannot be resolved based on thetheme
andthemeSpec
.backgroundColor
is unresolved because while it is a CSS property registered inthemeSpec
, the'not.a.valid.path'
theme value is not resolvable undertheme.colors
.padding
is unresolved because it is not a CSS property registered inthemeSpec
despite having a valid theme value.
-
Theme renderer: A program that resolves themed styles based on the provided theme and theme spec, and renders the CSS to DOM.
-
Theme system: A system of programs that supports specifying the theme spec, creating and validating the relating theme, resolving and rendering themed styles to CSS.
-
Style: A declaration for styling HTML elements via CSS. Authored in JS as style objects or style rules. The CSS-in-JS syntax is fairly ubiquitous across CSS frameworks and we provide an example to highlight notable syntax and features.
Example
const style = { color: 'red', fontSize: 14, // CSS properties are camel-cased, and may accept unitless values ':hover': { // CSS pseudo class color: 'yellow', ':active': { // can be further nested (equivalent to ':hover:active') color: 'blue', }, }, '::before': { // CSS pseudo element content: '" "', // nested quotes to set string content }, '[checked="true"]': { // CSS attribute selector color: 'yellow', '[target]': { // can be further nested (equivalent to '[checked="true"][target]') color: 'blue', }, }, '> .some-class': { // CSS child selector color: 'yellow', '> #some-id': { // can be further nested (equivalent to '> .some-class > #some-id') color: 'blue', }, }, '& .some-class': { // CSS "self" selector color: 'yellow', ':hover': { // can be further nested (equivalent to '& .some-class:hover') color: 'blue', }, }, };
-
Style object: A style represented as a JS object.
Example
const style = { color: 'red', padding: 8, ':hover': { color: 'yellow', }, };
-
Style rule: A style represented as a JS function that returns a style object. This is useful to represent style as a function of state.
Example
const rule = (props) => ({ color: props.isActive ? 'blue' : 'yellow', padding: props.isPadded ? 8 : 0, }); console.log(rule({isActive: true})); // {color: 'blue', padding: 0} console.log(rule({isPadded: true})); // {color: 'yellow', padding: 8}
-
Style props: an object used as an argument for a style rule.
-
Global styles: refers to global style objects that are usually defined once and rendered to the global style sheet.
Example
const globalStyles = { '*': { boxSizing: 'border-box', }, 'body': { margin: 0, padding: 0, }, 'a': { color: 'blue', }, 'a:hover': { color: 'yellow', }, '.vendor-classname': {...} }
-
Responsive style: when an array of breakpoints are provided, responsive styles can be expressed in convenient array notation to render media queries.
Example
Given the following responsive breakpoints (
min-width
-based):const breakpoints = ['400px', '800px'];
And a responsive style:
const responsiveStyle = { color: ['black', 'blue', 'yellow'], margin: [4, 8, 16], };
Yields the following rendered CSS:
.x { color: black; margin: 4px; } @media (min-width: 400px) { .x { color: blue; margin: 8px; } } @media (min-width: 800px) { .x { color: yellow; margin: 16px; } }
-
Atomic CSS: Every CSS property/value pair is generated as a unique CSS class. This allows HTML elements to reuse and share class definitions, which is a useful strategy to limit and reuse rendered CSS.
Example
Given the following HTML and (non-atomic) CSS,<div class="x" /> <div class="y" />
.x { color: red; padding: 8px; } .y { color: red; padding: 4px; }
The following HTML would be equivalent using atomic CSS.
<div class="a b" /> <div class="a c" />
.a { color: red; } .b { padding: 8px; } .c { padding: 4px; }
-
CSS class: See CSS (MDN).
-
CSS variable: See CSS variable (MDN).
-
CSS keyframe: See CSS keyframe (MDN).
-
CSS property: See CSS property (MDN).
-
CSS selector: See CSS selector (MDN).
-
CSS value: See CSS value (MDN).
uinix-theme is originally inspired by the ideas in theme-ui, and evolves these ideas into framework-agnostic and fully configurable APIs, implemented via fela.
uinix-theme approaches theme systems with the following principles:
- Fully-configurable: Enable consumers to own their own spec instead of providing an opinionated one.
- Framework-agnostic: Solve the domain problem in JS and not in specific frameworks.
- Build-free: APIs are usable without the need for a build system (e.g. directly usable in browsers as plain JS).
- Update-free: APIs are intended to be stable, imparting confidence for both maintainers and consumers of the project.
uinix-theme ships with Typescript declarations, compiled and emitted when installed. The source code is pure Javascript.
uinix-theme adheres to semver starting at 1.0.0.
Note: uinix-theme is a JS-first project. Typescript types are provided as a supplementary convenience for the TS community. Changes in typings will always be treated as semver fixes.
Node 18+ is required for development.
Install dependencies with npm i
and run tests with npm test
. You can also run other NPM scripts (e.g. lint
) from the root of the monorepo.