Hanzo GUI

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/config
import { 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 config

The 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:

PropTypeDefaultRequired
defaultThemestring-
disableInjectCSSboolean--
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 GuiBuildOptions

And then run:

npx @hanzogui/cli generate

See 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:

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:

PropTypeDefaultRequired
disableSSRboolean--
defaultFontstring--
mediaQueryDefaultActiveRecord<string, boolean>--
addThemeClassNamefalse | 'html' | 'body'-
disableRootThemeClassboolean--
selectionStyles(theme) => ({ backgroundColor: Variable | string; color: Variable | string })-
shouldAddPrefersColorThemesbooleantrue-
onlyAllowShorthandsboolean-
allowedStyleValuesAllowedStyleValuesSetting--
autocompleteSpecificTokensboolean | 'except-special'--
fastSchemeChangeboolean-
webContainerTypestringinline-size-

Type strictness

allowedStyleValues

  • false (default): allows any string (or number for styles that accept numbers)
  • strict: only allows tokens for any token-enabled properties
  • strict-web: same as strict but allows for web-specific tokens like auto/inherit
  • somewhat-strict: allow tokens or:
    • for space/size: string% or numbers
    • for radius: number
    • for zIndex: number
    • for color: named colors or rgba/hsla strings
  • 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

On this page