Files
zeekay c183da5389 Drop webpack from the stack
The loader and the Next plugin only existed to drive webpack; Next runs
on Turbopack and Vite has its own plugin. kitchen-sink and the
integration app serve through vite, the optimizer suite drops the
webpack name, and its fixture packages join the workspace so a fresh
checkout links @my/ui.
2026-09-06 08:17:57 -07:00

467 lines
12 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Themes
description: Create themes and sub-themes
---
Themes map neatly to CSS variables: they are objects whose values you want to
contextually change at any point in your React tree. They are used either as the
first lookup for `$` prefixed style values, or with the `useTheme` hook
directly. Gui allows nesting themes - both the definition and at runtime. At
runtime Gui resolves theme values upwards, ultimately all the way back to
tokens.
For the recommended theme setup, see the [Config v5 docs](/docs/core/config-v5).
Once you've learned the basics here, be sure to
[check out the ThemeBuilder guide](/docs/guides/theme-builder) for generating
more interesting theme suites.
If you want to style `hanzogui` components: see
[Styling `hanzogui` UI components](#styling-hanzogui-components).
<Notice theme="green">
If you want a copy-paste theme generation setup, [try this
gist](https://gist.github.com/natew/3be503cc5990a1142d17ffadf86a134f) for a
well-structured example.
</Notice>
You define a theme like this:
```tsx
const dark = {
background: '#000',
color: '#fff',
// define any key to any string or number value
}
```
If you use tokens, you can share values from tokens down to themes. Tokens act
as fallback values for themes, like global CSS variables vs scoped ones:
```tsx
const tokens = createTokens({
color: {
black: '#000',
white: '#fff',
},
})
// theme:
const dark = {
background: tokens.color.black,
color: tokens.color.white,
}
```
### Sub-themes
One of the unique powers of Gui is theme nesting. Define a theme with a
name in the form of `parentName_subName` and Gui will let you nest themes,
with both `parentName` and `subName` being valid theme names.
You can do this as many times as you'd like. Here's an example of having three
levels:
- `dark_green_subtle`
- `light_green_subtle`
```tsx
<Theme name="dark">
<Theme name="green">
<Button theme="subtle">Hello world</Button>
</Theme>
</Theme>
```
You can also access a specific sub-theme more specifically:
```tsx
<Theme name="dark">
<Button theme="green_subtle">Hello world</Button>
</Theme>
```
In general you want your themes to all be the same shape - the same named keys
and typed values - but sub-themes can be sub-sets of parent themes. The
`useTheme` hook and style system will resolve missing keys upwards to parent
themes, and ultimately to tokens.
#### Component themes
Every Gui `styled()` component looks for its own specific theme if you pass
it the `name` property. For example:
```tsx
import { View, styled } from '@hanzo/gui' // or '@hanzogui/core'
const Circle = styled(View, {
name: 'Circle',
backgroundColor: '$background',
})
```
The `name` attribute will be removed from the defaultProps and used internally
by Gui to check for a sub-theme that ends with `_Circle`.
Now you can create the default theme for all Circle components at any level of
nesting:
```tsx
const dark_Circle = {
background: 'darkred',
color: 'white',
}
const light_Circle = {
background: 'lightred',
color: 'black',
}
```
<Notice theme="blue">Component themes must have the first letter capitalized.</Notice>
- `dark_Circle`
- `dark_green_Circle`
- `dark_green_subtle_Circle`
This is an incredibly powerful and unique feature that allows authors of UI
components control over design, while still letting users customize them
completely.
---
### Styling Gui UI components
Gui comes in two parts: a core library and a full UI kit. The core library
(`@hanzogui/core`) is flexible and doesn't have many rules. But the full UI kit
(`hanzogui`) has some standard ways of doing things. This helps make everything
work well together.
In the `hanzogui` UI kit, all components use these main theme keys:
- `background`: for background colors
- `color`: for text colors
- `borderColor`: for border colors
- `shadowColor`: for shadow colors
- `placeholderColor`: for placeholder text colors (doesn't change when you
interact with it)
It also uses special versions of these for when you hover, press, or focus on
something. For example, `backgroundHover` or `colorPress`.
These keys help standardize how you style components, and make for easy
re-theming. They are also optional, if you find the theme system too complex for
your use case, you can always just use plain old style props
...plus all the pseudo variants for each, eg, `backgroundHover`,
`backgroundPress`, and `backgroundFocus`.
This means that you can easily re-theme `hanzogui`'s UI kit and your own
components together in both light and dark mode.
A minimal theme might look like this:
```tsx
const dark = {
// Standard keys for all components
background: '#000',
backgroundHover: '#111',
backgroundPress: '#222',
backgroundFocus: '#333',
backgroundStrong: '#444',
backgroundTransparent: 'rgba(0, 0, 0, 0.5)',
color: '#fff',
colorHover: '#eee',
colorPress: '#ddd',
colorFocus: '#ccc',
colorTransparent: 'rgba(255, 255, 255, 0.5)',
borderColor: '#555',
borderColorHover: '#666',
borderColorFocus: '#777',
borderColorPress: '#888',
placeholderColor: '#999',
outlineColor: '#aaa',
// Custom tokens like "brand"
brandBackground: '#000', // You can add your own tokens like "brand"
brandColor: '#fff', // and use them in your components
}
const light = {
// Standard keys for all components
background: '#fff',
backgroundHover: '#f5f5f5',
backgroundPress: '#e0e0e0',
backgroundFocus: '#d5d5d5',
backgroundStrong: '#ccc',
backgroundTransparent: 'rgba(255, 255, 255, 0.5)',
color: '#000',
colorHover: '#111',
colorPress: '#222',
colorFocus: '#333',
colorTransparent: 'rgba(0, 0, 0, 0.5)',
borderColor: '#444',
borderColorHover: '#555',
borderColorFocus: '#666',
borderColorPress: '#777',
placeholderColor: '#888',
outlineColor: '#999',
// Custom tokens like "brand"
brandBackground: '#000', // You can add your own tokens like "brand"
brandColor: '#fff', // and use them in your components
}
```
You can of course do all of this yourself in your own design system with
`styled`:
If you are building a component with more than one sub-components, you can
follow this pattern:
```tsx
import { GetProps, View, Text, styled } from '@hanzo/gui' // or '@hanzogui/core'
const ButtonFrame = styled(View, {
name: 'Button',
backgroundColor: '$background',
})
const ButtonText = styled(Text, {
name: 'ButtonText',
color: '$color',
})
type ButtonProps = GetProps<typeof ButtonFrame>
// styleable() wraps a functional component so it can be further styled with styled()
// see /docs/core/styled#styleable for full documentation
export const Button = ButtonFrame.styleable<ButtonProps>(
({ children, ...props }, ref) => {
return (
<ButtonFrame ref={ref} {...props}>
<ButtonText>{children}</ButtonText>
</ButtonFrame>
)
}
)
```
And now you can add two themes: `dark_Button` and `dark_ButtonText`, and
override their default styles.
## Full Example
Let's start with an example of inline styling with a subset of the
configuration:
```tsx
import { GuiProvider, createTokens, createGui, View, Theme } from '@hanzo/gui'
const tokens = createTokens({
color: {
darkRed: '#550000',
lightRed: '#ff0000',
},
// ... see configuration docs for required tokens
})
const config = createGui({
tokens,
themes: {
dark: {
red: tokens.color.darkRed,
},
light: {
red: tokens.color.lightRed,
},
},
})
export const App = () => (
<GuiProvider config={config} defaultTheme="light">
<View backgroundColor="$red" />
<Theme name="dark">
<View backgroundColor="$red" />
</Theme>
</GuiProvider>
)
```
In this example we've set up darkRed and lightRed variables and a dark and light
theme that use those variables. Gui will handle defining:
```css
:root {
--colors-dark-red: #550000;
--colors-light-red: #ff0000;
}
.tui_dark {
--red: var(--colors-dark-red);
}
.tui_light {
--red: var(--colors-light-red);
}
```
Which will automatically apply at runtime, or can be gathered for use in SSR
using `Gui.getCSS()`.
Finally, the compiler on web will extract your views roughly as so:
```tsx
export const App = () => (
<Provider defaultTheme="light">
<div className="baCo-2nesi3" />
<Theme name="dark">
<div className="baCo-2nesi3" />
</Theme>
</Provider>
)
// CSS output:
// .color-2nesi3 { background-color: var(--red); }
```
## Ensuring valid types
Here's what we've landed on which helps ensure everything is typed properly.
Keep themes in a separate `themes.ts` file, and structure it like this:
```tsx
import { tokens } from './tokens'
const light = {
background: '#fff',
backgroundHover: tokens.color.gray3,
backgroundPress: tokens.color.gray4,
backgroundFocus: tokens.color.gray5,
borderColor: tokens.color.gray4,
borderColorHover: tokens.color.gray6,
color: tokens.color.gray12,
colorHover: tokens.color.gray11,
colorPress: tokens.color.gray10,
colorFocus: tokens.color.gray6,
shadowColor: tokens.color.grayA5,
shadowColorHover: tokens.color.grayA6,
}
// note: we set up a single consistent base type to validate the rest:
type BaseTheme = typeof light
// the rest of the themes use BaseTheme
const dark: BaseTheme = {
background: '#000',
backgroundHover: tokens.color.gray2Dark,
backgroundPress: tokens.color.gray3Dark,
backgroundFocus: tokens.color.gray4Dark,
borderColor: tokens.color.gray3Dark,
borderColorHover: tokens.color.gray4Dark,
color: '#ddd',
colorHover: tokens.color.gray11Dark,
colorPress: tokens.color.gray10Dark,
colorFocus: tokens.color.gray6Dark,
shadowColor: tokens.color.grayA6,
shadowColorHover: tokens.color.grayA7,
}
const dark_translucent: BaseTheme = {
...dark,
background: 'rgba(0,0,0,0.7)',
backgroundHover: 'rgba(0,0,0,0.5)',
backgroundPress: 'rgba(0,0,0,0.25)',
backgroundFocus: 'rgba(0,0,0,0.1)',
}
const light_translucent: BaseTheme = {
...light,
background: 'rgba(255,255,255,0.85)',
backgroundHover: 'rgba(250,250,250,0.85)',
backgroundPress: 'rgba(240,240,240,0.85)',
backgroundFocus: 'rgba(240,240,240,0.7)',
}
export const allThemes = {
dark,
light,
dark_translucent,
light_translucent,
} satisfies { [key: string]: BaseTheme }
// note: `satisfies` was introduced with TypeScript 4.9
```
## Dynamic Themes
Sometimes you want to defer loading themes, or change existing theme values at
runtime. Gui exports three helpers for this in the package `@hanzogui/theme`
which exports `addTheme`, `updateTheme`, and `replaceTheme`.
### addTheme
<HeroContainer>
<AddThemeDemo />
</HeroContainer>
```tsx hero template=AddTheme
```
### updateTheme
<HeroContainer>
<UpdateThemeDemo />
</HeroContainer>
```tsx hero template=UpdateTheme
```
### replaceTheme
<HeroContainer>
<ReplaceThemeDemo />
</HeroContainer>
```tsx hero template=ReplaceTheme
```
### Notes
- Dynamic themes only work on the client side and will be ignored on the server
side.
- The difference between `updateTheme` and `replaceTheme` is that `replaceTheme`
will replace the entire theme, while `updateTheme` will only update the values
that are passed in.
### Advanced Optimization
You can configure Gui to not send any themes JS to the client side, so long
as your are serving the resulting css file from the `getCSS` call on initial
load of your app (SSR).
To enable this you need to have your bundler tree shake away the themes object
you'd typically pass to `createGui` for the client bundle. Note this is a
somewhat advanced optimization and not necessary to do right away.
```tsx
import { themes as themesIn } from './your-themes-file'
// We leave this value empty for production client side bundles to save on bundle size.
// The condition must be a constant your bundler folds for the client build:
// `@hanzogui/vite-plugin` defines `process.env.GUI_IS_CLIENT` there; with
// another bundler, define it yourself for the client build only.
// If you pass an empty themes object Gui will try to hydrate by scanning CSS in browser environments.
// It typically takes low single-digit ms to scan and can save significantly on JS size.
const themes =
process.env.GUI_IS_CLIENT && process.env.NODE_ENV === 'production'
? ({} as typeof themesIn)
: themesIn
export const config = createGui({
themes,
// ...
})
```