Craft perfect themes that fit your brand.
Design and prototype quicker by getting an overview of all the UI components you use.
- đź–Ľ Preview how all the components and their variants look side-by-side on one organized page
- 🎨 Edit design tokens and see changes live
Add package into your project.
Install as dev dependency:
npm install -D @duotone/react
or
yarn add -D @duotone/react
or
pnpm add -D @duotone/react
Next, create default config using:
npx duotone init
Add serve script to your package.json
scripts:
{
"scripts": {
"duotone": "duotone serve",
"duotone:build": "duotone build"
}
}
npm run duotone
or
yarn duotone
or
pnpm duotone
Open the duotone app in your browser. By default it's at
http://localhost:7890
You'll see the example setup of duotone!
Now let's add your own 👇👇👇
Config directory gets created at the root of your project: .duotone
In it are two main config files:
config.mjs
- main config file for setuppreview.jsx
- components and preview
You can safely remove example template files: components.jsx
and theme.js
.
Exports default configuration object
// config.mjs
/**
* @type {import('@duotone/react').UserConfig}
*/
export default {
/**
* Object with theme setup (required)
*/
themes: {
lightTheme: '../theme.js',
},
/**
* Name of the UI kit (optional)
*/
name: 'Our UI',
/**
* Dev config (optional)
*/
// Port of the duotone server
port: 7890,
// Folder with publishable duotone after running "duotone build"
outDir: 'duotone-dist',
// Public path of published duotone
base: '/',
// File with Vite configuration
viteConfig: '../vite.config.js',
}
Object where keys are the theme names and value is a path to the theme module. Path is relative to the config file.
If just one theme is set to be exported from a given file it's assumed it's exported as default.
If multiple themes come form the file it's assumed, they are named exports. The theme object keys must match the named exports!
Examples:
Different theme paths
// config.mjs
{
themes: {
light: '../light-theme.ts',
dark: '../dark-theme.ts'
}
}
mean this theme structure:
// light-theme.ts
export default {
text: '#000',
}
// dark-theme.ts
export default {
text: '#FFF',
}
Same theme path
// config.mjs
{
themes: {
light: '../theme.ts',
dark: '../theme.ts'
}
}
means this theme structure:
// theme.ts
export const light = {
text: '#000',
}
export const dark = {
text: '#FFF',
}
If you want to name themes different to named exports
or add specific preview styles for a theme, use this config format:
// config.mjs
{
themes: {
lightTheme: {
name: 'light',
path: '../theme.ts',
previewStyles: {
background: '#ccc',
color: '#000',
panelBackground: '#FFF',
}
}
}
}
Should be named preview
with either .jsx
or .tsx
extension
Export as default
or as a named export components
Object with keys as component names and values as ComponentsConfig.
Example:
// preview.jsx
export const components = {
Button: {
render: props => <Button>Button</Button>
variants: {
Size: {
prop: 'size',
options: ['sm', 'md', 'lg']
}
}
}
}
Define and export a createTheme
function to customize how the updated theme will get generated.
By default it merges (deep) all updated tokens into original theme.
The funciton accepts two arguments:
tokens
- object with updated tokensthemePack
- currently selected theme information (usethemePack.theme
ThemePack
export const createTheme = (tokens: ThemeTokens, themePack: ThemePack) =>
merge({}, themePack.tokens, tokens)
Export a provider that wraps around the whole preview tree. Use it to pass theme context, add internalization or run code that add global styling.
Provider receives the updated theme object as prop. Pass it e.g. to theme provider to style the components.
// preview.jsx
export const Provider = ({theme, children}) => (
<ThemeProvider theme={theme}>
{children}
</Theme>
)
You can customize the preview styling
To do so for all themes, export previewStyles
from preview file.
StylesConfig
// preview.jsx
export const previewStyles = {
background: '#f8f9fa',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontColor: '#151718',
primaryColor: '#3e63dd',
renderPanel: {
background: '#fff',
fontColor: '#151718',
},
}
To style each theme preview individually, add previewStyles
to theme config object
// config.mjs
{
themes: {
lightTheme: {
name: 'light',
path: '../theme.ts',
previewStyles: {
background: '#f8f9fa',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontColor: '#151718',
primaryColor: '#3e63dd',
renderPanel: {
background: '#fff',
fontColor: '#151718',
},
}
}
}
}
The bulding and dev server are powered by Vite and esbuild. These are much faster than Webpack and babel, giving superior DX.
Building Typescript and importing static files is handled automatically. Supports only ES-modules.
The code is heavily based on Ladle, which is a great alternative to Storybook.
Powered by React, jotai for state management.
Components preview lives in it's own pacakge @duotone/preview.
You can install and use it on it's own as a React component!
Install
npm install @duotone/preview
or
yarn add @duotone/preview
or
pnpm add @duotone/preview
Use
import DuotonePreview from '@duotone/preview'
export default () => (
<DuotonePreview kitName="My UI" components={components} previewStyles={previewStyles} />
)
See API for components - ComponentsConfig and previewStyles - StylesConfig
You can use params to configure main server:
Usage
$ duotone serve
Options
-p, --port port of the duotone server [default: 7890]
-config config directory [default: .duotone]
-viteConfig file with Vite configuration
A component preview config
type ComponentConfig = {
/**
* Render function of the component.
* Pass props to the element that expects variant props.
*/
render?: (props?: Record<string, any>) => React.ReactNode
/**
* List of variants to render in preview
*/
variants?: {
// Name of the variant
name: string
// Prop that controls the variant on the component
prop?: string
/**
* List of variant options
* Pass the option value or an object that let's you control
* the option name and how it renders individually.
*/
options: (string | number | boolean | { name: string; render: () => React.ReactNode })[]
}[]
}
Components preview config object
type ComponentConfig = {
[ComponentName: string]: ComponentConfig
}
Style the preview
type StylesConfig = {
fontFamily?: string // Preview font family
fontSize?: string // Base preview font size
fontColor?: string // Text color
background?: string // Preview background color
primaryColor?: string // Primary color for highlights
// Panel that renders the component options
renderPanel?: {
background?: string // Background of the render panel
fontColor?: string // Text colors inside the render panel
}
}
Provider that wraps previewed components
type Provider = (
props: React.PropsWithChildren<{ theme: ThemeTokens }>
) => React.ComponentElement<any, any>
Fn to create dervied themes
type CreateTheme = (tokens: ThemeTokens, themePack: ThemePack) => {} | string
Theme store object
type ThemePack<T = {}> = {
name: string
theme: T
tokens: ThemeTokens
previewStyles?: StylesConfig
}
Matej Lauko (@matejlauko)
MIT