Configuration
Set up media queries, tokens, themes, and more.
Configuration in Hanzo GUI controls things like tokens, themes, media queries, animations, shorthands, and settings.
First, create a gui.config.ts file.
We recommend starting with @hanzogui/config/v5, our preset
configuration with sensible defaults, Tailwind-aligned shorthands, and a complete theme
system.
npm install @hanzogui/configimport { defaultConfig } from '@hanzogui/config/v5'
import { createGui } from '@hanzo/gui'
export const config = createGui({
...defaultConfig,
media: {
...defaultConfig.media,
// add your own media queries here, if wanted
},
})
type OurConfig = typeof config
declare module '@hanzo/gui' {
interface GuiCustomConfig extends OurConfig {}
}We find most people are best off with this as it saves a lot of time, shares shorthands with Tailwind, and has a lot of refinement. For more on our default configuration, go to the next page.
Here's an example of a stripped down configuration to get a feel for the most common concepts:
import { createGui, getConfig } from '@hanzo/gui'
export const config = createGui({
// tokens work like CSS Variables (and compile to them on the web)
// accessible from anywhere, never changing dynamically:
tokens: {
// width="$sm"
size: { sm: 8, md: 12, lg: 20 },
// margin="$sm"
space: { sm: 4, md: 8, lg: 12 },
// radius="$none"
radius: { none: 0, sm: 3 },
color: { white: '#fff', black: '#000' },
},
// themes are like CSS Variables that you can change anywhere in the tree
// you use <Theme name="light" /> to change the theme
themes: {
light: {
bg: '#f2f2f2',
color: '#000',
},
dark: {
bg: '#111',
color: '#fff',
},
// sub-themes are a powerful feature of gui, explained later in the docs
// user theme like <Theme name="dark"><Theme name="blue">
// or just <Theme name="dark_blue">
dark_blue: {
bg: 'darkblue',
color: '#fff',
},
},
// media query definitions can be used as style props or with the useMedia hook
// but also are added to "group styles", which work like Container Queries from CSS
media: {
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
short: { maxHeight: 820 },
hoverable: { hover: 'hover' },
touchable: { pointer: 'coarse' },
},
shorthands: {
// <View px={20} />
px: 'paddingHorizontal',
},
// there are more settings, explained below:
settings: {
disableSSR: true,
allowedStyleValues: 'somewhat-strict-web',
},
})
// now, make your types flow nicely back to your `@hanzo/gui` import:
type OurConfig = typeof config
declare module '@hanzo/gui' {
interface GuiCustomConfig extends OurConfig {}
}In this guide we import everything from @hanzo/gui, but if you are using the lower-level
style library only, you can change the imports in this guide out for @hanzogui/core,
which is a strict subset.
Finally, pass your config export to a <GuiProvider /> at the root of
your app:
import { GuiProvider, View } from '@hanzo/gui'
import { config } from './gui.config.ts'
export default () => (
<GuiProvider config={config}>
<View margin="$sm" />
</GuiProvider>
)You're all set up!
To avoid issues with hot reloading and circular imports, make sure to only import your
gui.config.ts once near the root of your app. If you need to access the
configuration in other files, you generally do this just by using props on your styled
components, or with hooks like useMedia or useTheme. We do have some getters like
getTokenValue, and if you really need to access the full configuration object (though
we don't find it necessary in most cases), you can use getConfig.
In more detail
Let's start with an example of a more complete gui.config.ts:
import { createFont, createGui, createTokens, isWeb } from '@hanzo/gui'
// To work with the gui UI kit styled components (which is optional)
// you'd want the keys used for `size`, `lineHeight`, `weight` and
// `letterSpacing` to be consistent. The `createFont` function
// will fill-in any missing values if `lineHeight`, `weight` or
// `letterSpacing` are subsets of `size`.
const systemFont = createFont({
family: isWeb ? 'Helvetica, Arial, sans-serif' : 'System',
size: {
1: 12,
2: 14,
3: 15,
},
lineHeight: {
// 1 will be 22
2: 22,
},
weight: {
1: '300',
// 2 will be 300
3: '600',
},
letterSpacing: {
1: 0,
2: -1,
// 3 will be -1
},
// (native only) swaps out fonts by face/style
face: {
300: { normal: 'InterLight', italic: 'InterItalic' },
600: { normal: 'InterBold' },
},
})
// Set up tokens
// The keys can be whatever you want, but if using `@hanzo/gui` you'll want 1-10:
const size = {
0: 0,
1: 5,
2: 10,
// ....
}
export const tokens = createTokens({
size,
space: { ...size, '-1': -5, '-2': -10 },
radius: { 0: 0, 1: 3 },
zIndex: { 0: 0, 1: 100, 2: 200 },
color: {
white: '#fff',
black: '#000',
},
})
const config = createGui({
fonts: {
heading: systemFont,
body: systemFont,
},
tokens,
themes: {
light: {
bg: '#f2f2f2',
color: tokens.color.black,
},
dark: {
bg: '#111',
color: tokens.color.white,
},
},
media: {
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
short: { maxHeight: 820 },
hoverable: { hover: 'hover' },
touchable: { pointer: 'coarse' },
},
// Shorthands
// Adds <View m={10} /> to <View margin={10} />
// See Settings section on this page to only allow shorthands
// Be sure to have `as const` at the end
shorthands: {
px: 'paddingHorizontal',
f: 'flex',
m: 'margin',
w: 'width',
} as const,
// Change the default props for any styled() component with a name.
// We are discouraging the use of this and have deprecated it, prefer to use
// styled() on any component to change its styles.
defaultProps: {
Text: {
color: 'green',
},
},
})
type AppConfig = typeof config
// this will give you types for your components
// note - if using your own design system, put the package name here instead of gui
declare module '@hanzo/gui' {
interface GuiCustomConfig extends AppConfig {}
// if you want types for named group styling props (e.g. $group-card-hover),
// define your group names here:
interface TypeOverride {
groupNames(): 'card' | 'header' | 'sidebar'
}
}
export default configThe createGui function receives a configuration object with properties:
animations: Configurable animation drivers.media: Cross-platform, typed media queries.themes: Define themes to style contextually anywhere in the tree, much like CSS variables.tokens: Your base tokens are much like static CSS variables.settings: Many options for strictness and style behavior.shorthands: Define shorter names for any style property.
On Android you need to set the face option in createFont or else fonts won't pick up
different weights, due to a React Native restriction.
Note, for @hanzo/gui (not core), it expects you to define a true token that
maps to your default size, this way it knows what token to use by default. So
you'd do something like this:
export const tokens = createTokens({
size: {
small: 20,
medium: 30,
true: 30, // note true = 30 just like medium, your default size token
large: 40,
},
space: {
small: 10,
medium: 20,
true: 20, // same goes for space and other token categories
large: 30,
},
})If using the compiler, your gui.config.ts is parsed at build-time. For this reason, we recommend keeping it relatively simple. Avoid importing heavy dependencies.
GuiProvider
With your config set up, import it near the root of your app and pass it to
GuiProvider:
import { GuiProvider } from '@hanzo/gui'
import { config } from './gui.config'
export default function App() {
return (
<GuiProvider config={config} defaultTheme="light">
<AppContents />
</GuiProvider>
)
}GuiProvider accepts a few properties:
| Prop | Type | Default | Required |
|---|---|---|---|
| defaultTheme | string | - | ✓ |
| disableInjectCSS | boolean | - | - |
| insets | { top?: number; bottom?: number; left?: number; right?: number } | - | - |
By default, Hanzo GUI injects the CSS for your configuration on the client-side
into document.head, but you probably will want something better for
production. To do this, pass true to disableInjectCSS on GuiProvider, and
then do one of the following three options:
If your framework has server-side layouts, you can just render it inline:
import { config } from './gui.config'
export default () => (
<html>
<head>
<style
dangerouslySetInnerHTML={{
__html: config.getCSS(),
}}
/>
</head>
<body>
<Slot />
</body>
</html>
)To optimize a bit more so you share a single CSS file between all pages, you can
use one of our bundler plugins' outputCSS setting, like so:
import { guiPlugin } from '@hanzogui/vite-plugin'
export default {
plugins: [
guiPlugin({
config: './src/gui.config.ts',
outputCSS: './src/gui.generated.css',
}),
],
}And then you'll want to import the resulting gui.generated.css into your app.
As final option, you can also generate it yourself with the CLI. First create a
gui.build.ts:
import type { GuiBuildOptions } from '@hanzo/gui'
export default {
components: ['@hanzo/gui'],
config: './config/gui.config.ts',
outputCSS: './gui.generated.css',
} satisfies GuiBuildOptionsAnd then run:
npx @hanzogui/cli generateSee the CLI Guide for more information on the
generate command and other CLI tools.
Tokens
Tokens are inspired by the Theme UI spec. They are mapped to CSS variables at build time. You can read about them in more depth on the tokens page.
Font tokens
The font tokens are a bit special and are created with createFont:
import { isWeb } from '@hanzo/gui'
const interFont = createFont({
family: isWeb ? 'Inter, Helvetica, Arial, sans-serif' : 'Inter',
size: {
1: 12,
2: 14,
3: 15,
// ...
},
lineHeight: {
1: 17,
2: 22,
3: 25,
// ...
},
weight: {
4: '300',
6: '600',
},
letterSpacing: {
4: 0,
8: -1,
},
// because android handles fonts differently, you need to map the weight
// to the actual name of the font in the font-file
// you can get the name with `otfinfo`: otfinfo --family Inter.ttf
face: {
400: { normal: 'Inter', italic: 'Inter-Italic' },
500: { normal: 'InterBold', italic: 'InterBold-Italic' },
},
})We use numbered keys as an example, but you can use any strings you'd like. The optional
default styles in @hanzo/gui make use of number keys 1-10.
This gives you a lot of power over customizing every aspect of your design based on each font family. In other styling libraries that follow the Theme UI spec, you generally don't group your size/lineHeight/weight/etc tokens by the family, which means you are forced to choose a single vertical rhythm no matter the font.
Custom Fonts on Native
If you are using a custom font for native, you need to load your fonts for React Native to recognize them. Hanzo GUI doesn't really touch this area, instead you'll use Expo or React Native directly, something like this:
import { useFonts } from 'expo-font'
function App() {
const [loaded] = useFonts({
Inter: require('@hanzogui/font-inter/otf/Inter-Medium.otf'),
InterBold: require('@hanzogui/font-inter/otf/Inter-Bold.otf'),
})
useEffect(() => {
if (loaded) {
// can hide splash screen here
}
}, [loaded])
if (!loaded) {
return null
}
return <MyApp />
}Non-font tokens
The rest of the tokens categories besides font are flatter. The space and
size generally share keys, and that space can generally use negative keys as
well.
// passed into createGui
const tokens = createTokens({
color: {
white: '#fff',
black: '#000',
},
})You access tokens then by using $ prefixes in your values. Hanzo GUI knows which
tokens to use based on the style property you use.
const App = () => (
<Text fontSize="$lg" lineHeight="$lg" fontFamily="$body" color="$white">
Hello world
</Text>
)One final note: using tokens with themes. Tokens are considered a "fallback" to themes, so any values you define in your theme will override the token. The next section will explain this further.
Configuring tokens
There are a few settings that control how strict your style values are allowed
to be, which are handled by the settings option of createGui. See the
settings below.
Themes
Themes live one level below tokens. Tokens are your variables, where themes use those tokens to create consistent, generic properties that you then typically use in shareable components. Themes should generally only deal with colors.
Hanzo GUI components in general expect a set of theme keys to be defined like the following, but you can deviate if you create your own design system.
const config = createGui({
themes: {
light: {
background: '#fff',
backgroundHover: tokens.color.gray2,
// ...
color: tokens.color.gray10,
colorHover: tokens.color.gray9,
colorPress: tokens.color.gray8,
// ...
color1: tokens.color.gray1,
},
dark: {
background: '#000',
// ... matching the properties for light ^
},
},
// ... the rest of your configuration
})Passing tokens to themes will be smart about sharing CSS, but is not required.
You can then access theme values for any style value, either through styled() or through a Styled Component like View or Text:
const P = styled(Text, {
color: '$color12'
})
// or directly
<Text color="$color11" />One of the more powerful features in Hanzo GUI is nesting themes, you just define them like so:
const config = createGui({
themes: {
light: {
color1: '#fff',
// ...
},
dark: {
color1: '#000',
// ...
},
light_alert: {
background: '#e6ebbd',
},
dark_alert: {
background: '#3e3f33',
},
},
// ... the rest of your configuration
})Themes work just like CSS variables, they can be changed anywhere in the tree.
Sub-themes can be subsets of parent themes likewise as their values will fall
back to the parent theme. They can also be nested as deeply as you like - so
even light_alert_subtle is possible.
For more on themes, see the themes docs, and more advanced theme building library.
Media
For more full docs on media queries, see the useMedia docs page.
Animations
Hanzo GUI supports four animation drivers that can be swapped out per-platform:
@hanzogui/animations-css- CSS transition based animations (lightest)@hanzogui/animations-react-native- React Native Animated@hanzogui/animations-reanimated- Reanimated for advanced animations@hanzogui/animations-motion- Motion with WAAPI for web
See the Animations documentation for a full comparison and guide on choosing the right driver.
Add animations to createGui:
import { createAnimations } from '@hanzogui/animations-react-native'
// pass this exported `animations` to your `createGui` call:
export const animations = createAnimations({
bouncy: {
damping: 9,
mass: 0.9,
stiffness: 150,
},
lazy: {
damping: 18,
stiffness: 50,
},
})You can use different drivers per-platform - see the Platform-Specific Drivers section.
Shorthands
Shorthands are defined on createGui. Here's an example of a partial
shorthands configuration:
// the as const ensures types work with the optional `onlyAllowShorthands` option
const shorthands = {
ac: 'alignContent',
ai: 'alignItems',
als: 'alignSelf',
bblr: 'borderBottomLeftRadius',
bbrr: 'borderBottomRightRadius',
bg: 'backgroundColor',
br: 'borderRadius',
btlr: 'borderTopLeftRadius',
btrr: 'borderTopRightRadius',
f: 'flex',
// ...
} as const
export default createGui({
shorthands,
})Which will enable usage like:
<View br="$myToken" />where br expands into borderRadius.
Settings
You can pass a settings object to createGui:
| Prop | Type | Default | Required |
|---|---|---|---|
| disableSSR | boolean | - | - |
| defaultFont | string | - | - |
| mediaQueryDefaultActive | Record<string, boolean> | - | - |
| addThemeClassName | false | 'html' | 'body' | - | |
| disableRootThemeClass | boolean | - | - |
| selectionStyles | (theme) => ({ backgroundColor: Variable | string; color: Variable | string }) | - | |
| shouldAddPrefersColorThemes | boolean | true | - |
| onlyAllowShorthands | boolean | - | |
| allowedStyleValues | AllowedStyleValuesSetting | - | - |
| autocompleteSpecificTokens | boolean | 'except-special' | - | - |
| fastSchemeChange | boolean | - | |
| webContainerType | string | inline-size | - |
Type strictness
allowedStyleValues
false (default): allows any string (or number for styles that accept numbers)strict: only allows tokens for any token-enabled propertiesstrict-web: same as strict but allows for web-specific tokens like auto/inheritsomewhat-strict: allow tokens or:- for
space/size: string% or numbers - for
radius: number - for
zIndex: number - for
color: named colors or rgba/hsla strings
- for
somewhat-strict-web: same as somewhat-strict but allows for web-specific tokens
type AllowedValueSetting =
| boolean
| 'strict'
| 'somewhat-strict'
| 'strict-web'
| 'somewhat-strict-web'
type AllowedStyleValuesSetting =
| AllowedValueSetting
| {
space?: AllowedValueSetting
size?: AllowedValueSetting
radius?: AllowedValueSetting
zIndex?: AllowedValueSetting
color?: AllowedValueSetting
}autocompleteSpecificTokens
The VSCode autocomplete puts specific tokens above the regular ones, which leads to worse DX. If true this setting removes the specific token from types for the defined categories.
If set to except-special, specific tokens will autocomplete only if they don't
normally use one of the special token groups: space, size, radius, zIndex,
color.
Environment Settings
A few things in Hanzo GUI can be configured via environment variables. Doing it this way lets us avoid extra code being shipped to the client, as your bundler can tree shake the unused code easily when using environment variables. You should be sure to either use the Vite or Webpack Hanzo GUI plugins, or configure your bundler to define the environment variables for tree shaking. On native, it is not a concern as the extra size is minimal.
Tree Shaking Themes
Production Optimization - Theme JS can grow to 20KB or more. Since Hanzo GUI can hydrate themes from CSS variables, you can remove the themes JS from your client bundle for better Lighthouse scores.
This works because Hanzo GUI generates CSS variables for all theme values. On the client, it reads these from the DOM instead of needing the JS object.
This optimization only applies to SSR web apps (Next.js, One, Vite SSR). It doesn't apply to Metro/Expo or static sites. Note that v5 themes are already more optimized than v4, so this is mainly relevant if chasing the last few Lighthouse points.
Setup
import { defaultConfig, themes } from '@hanzogui/config/v5'
import { createGui } from '@hanzo/gui'
export const config = createGui({
...defaultConfig,
// only load themes on server - client hydrates from CSS
// for non-One Vite apps, use import.meta.env.SSR instead
themes: process.env.VITE_ENVIRONMENT === 'client' ? ({} as typeof themes) : themes,
})This optimization requires server-side rendering. The CSS must be rendered on the server
for the client to hydrate from it. Make sure you're using config.getCSS() or the
bundler plugin's outputCSS option.
Last updated on