mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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.
467 lines
12 KiB
Plaintext
467 lines
12 KiB
Plaintext
---
|
||
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,
|
||
// ...
|
||
})
|
||
```
|