Upgrading from v1 to v2
A comprehensive guide for migrating your Hanzo GUI project from v1 to v2
Hanzo GUI v2 aligns with modern web standards, improves performance, and simplifies APIs. The migration is moderate -- mostly removing deprecated APIs, renaming props, and updating your config. This guide walks through every change.
Prerequisites
Hanzo GUI 2 requires:
- React 19+
- React Native 0.81+ (with New Architecture support)
- TypeScript 5+
Upgrade these first before proceeding.
Update Dependencies
Bump all @hanzogui/* and @hanzo/gui packages to ^2.0.0:
# if using npm/yarn/pnpm, update all gui packages
npx hanzo-gui checkRemoved packages
@hanzogui/animations-moti-- use@hanzogui/animations-reanimated(same API)@hanzogui/image-next-- useImagefrom@hanzo/guidirectly
Add resolutions
In monorepos, you may want to add resolutions to avoid duplicate copies of core packages (critical for context/provider to work):
"resolutions": {
"@hanzogui/core": "^2.0.0",
"@hanzogui/web": "^2.0.0",
"@hanzo/gui": "^2.0.0"
}Config Migration (v4 to v5)
You don't have to migrate to Config v5, and it changes some default style behaviors that may cause some issues. It's easier to migrate to Hanzo GUI 2 first, then Config v5 after if you do choose to. Config v5 is mostly a superset of v5, but some media query names changed and some themes and colors are different.
Update imports
// before
import { defaultConfig } from '@hanzogui/config/v4'
// after
import { defaultConfig } from '@hanzogui/config/v5'
import { animations } from '@hanzogui/config/v5-css' // animations are now separateAnimations are no longer bundled with the config. Choose your driver:
@hanzogui/config/v5-css-- CSS transitions (smallest bundle, web-only)@hanzogui/config/v5-rn-- React Native Animated API@hanzogui/config/v5-reanimated-- Reanimated (best native performance)@hanzogui/config/v5-motion-- Motion (Web Animations API, experimental)
Add animations to config
import { defaultConfig } from '@hanzogui/config/v5'
import { animations } from '@hanzogui/config/v5-css'
import { createGui } from '@hanzo/gui'
export const config = createGui({
...defaultConfig,
animations,
})
export type Conf = typeof config
declare module '@hanzo/gui' {
interface GuiCustomConfig extends Conf {}
}Settings changes
All root-level createGui settings have moved into the settings object:
// before (v1)
createGui({
defaultFont: 'body',
disableRootThemeClass: true,
// ...
})
// after (v2)
createGui({
...defaultConfig,
settings: {
...defaultConfig.settings,
// your overrides
},
})Removed settings:
maxDarkLightNesting-- removed entirelycssStyleSeparator-- removed entirelythemeClassNameOnRoot-- handled byaddThemeClassNamedisableRootThemeClass-- now part ofsettings
Flex and position defaults
v5 changes two important defaults:
flexBasischanges fromautoto0(React Native standard)positionchanges fromrelativetostatic(browser default)
If your layout relies on the old behavior, restore it:
settings: {
...defaultConfig.settings,
styleCompat: 'legacy', // restores flexBasis: auto
defaultPosition: 'relative', // restores position: relative
}Otherwise, you may need to add position="relative" explicitly on containers that have absolutely-positioned children, and add flexBasis="auto" where needed.
Media query renames
$2xl→$xxl$2xs→$xxs$max2Xl→$max-xxl$maxXl→$max-xl$maxLg→$max-lg$maxMd→$max-md$maxSm→$max-sm
Max queries changed from camelCase to kebab-case. New height-based queries are also available ($height-sm, $height-md, etc.) and a $pointerTouch query.
Breakpoint values now match Tailwind CSS: 640, 768, 1024, 1280, 1536.
Colors and themes
Colors have been updated to Radix Colors v3 with slightly different values. Legacy colors are available at @hanzogui/colors/legacy.
v5 adds new color themes: orange, pink, purple, teal, gray, neutral.
// before (v1) - manual theme-builder
import { createThemes, defaultComponentThemes } from '@hanzogui/theme-builder'
const themes = createThemes({ ... })
// after (v2) - simplified
import { createV5Theme, defaultChildrenThemes } from '@hanzogui/themes/v5'
const themes = createV5Theme({
childrenThemes: {
...defaultChildrenThemes,
// add custom color themes
cyan: { light: cyan, dark: cyanDark },
},
})Component themes are off by default in v5. Use defaultProps in your config instead:
createGui({
...defaultConfig,
defaultProps: {
Button: { theme: 'accent' },
},
})Theme tree-shaking (SSR optimization)
On the client, you can skip loading theme JS and hydrate from CSS variables instead:
themes: process.env.VITE_ENVIRONMENT === 'client'
? ({} as typeof themes)
: themes,Once your v5 config is set up, run npx hanzo-gui generate-prompt and commit the output
to your repo. This helps AI assistants understand your Hanzo GUI config and can speed up
the rest of the migration.
Prop Renames
These are the most common find-and-replace changes.
animation to transition
// before
<View animation="bouncy" />
<Sheet.Overlay animation="lazy" />
// after
<View transition="bouncy" />
<Sheet.Overlay transition="lazy" />The TypeScript type also changed: AnimationProp to TransitionProp.
tag to render
// before
<View tag="nav" />
<View tag="button" />
// after
<View render="nav" />
<View render="button" />For tag="a", consider using the Anchor component instead.
Stack to View
// before
import { Stack, type StackProps } from '@hanzo/gui'
// after
import { View, type ViewProps } from '@hanzo/gui'themeInverse / <Theme inverse> to theme="accent"
// before
<Button themeInverse>Primary</Button>
<Theme inverse><Card>...</Card></Theme>
// after
<Button theme="accent">Primary</Button>
<Theme name="accent"><Card>...</Card></Theme>space / spaceDirection to gap
// before
<YStack space="$4" spaceDirection="both">
// after
<YStack gap="$4">onHoverIn / onHoverOut
// before
<View onHoverIn={handler} onHoverOut={handler} />
// after
<View onPointerEnter={handler} onPointerLeave={handler} />ellipse to numberOfLines
// before
<Text ellipse>Long text...</Text>
// after
<Text numberOfLines={1}>Long text...</Text>Shadow Migration
Replace React Native shadow props with CSS boxShadow:
// before
<View
shadowColor="$shadow3"
shadowRadius={20}
shadowOffset={{ height: 10, width: 0 }}
/>
// after
<View boxShadow="0 10px 20px $shadow3" />The format is x y blur color. Multiple shadows are comma-separated. Spread and inset are also supported.
Accessibility Props
All React Native accessibility props are replaced with web-standard ARIA equivalents:
accessibilityLabel→aria-labelaccessibilityRole→roleaccessibilityHint→aria-describedbyaccessibilityState={{ disabled }}→aria-disabledaccessibilityState={{ selected }}→aria-selectedaccessibilityState={{ checked }}→aria-checkedaccessibilityState={{ busy }}→aria-busyaccessibilityState={{ expanded }}→aria-expandedaccessibilityValue→aria-valuemin,aria-valuemax,aria-valuenow,aria-valuetextaccessibilityElementsHidden→aria-hiddenaccessibilityViewIsModal→aria-modalaccessibilityLiveRegion→aria-liveaccessible→tabIndex={0}focusable→tabIndexnativeID→id
Component API Changes
Input
Input now uses web-standard HTML attributes as the primary API:
// before
<Input
keyboardType="email-address"
secureTextEntry
returnKeyType="send"
textContentType="emailAddress"
onChangeText={(text) => setText(text)}
onKeyPress={(e) => {
if (e.nativeEvent.key === 'Enter') submit()
}}
editable={false}
/>
// after
<Input
inputMode="email"
type="password"
enterKeyHint="send"
autoComplete="email"
onChange={(e) => setText(e.target?.value ?? e.nativeEvent?.text ?? '')}
onKeyDown={(e) => {
if (e.key === 'Enter') submit()
}}
readOnly
/>The old React Native props still work but are deprecated.
Image
Image now uses web-standard src instead of React Native's source:
// before
<Image
source={{ uri: 'https://example.com/photo.jpg', width: 200, height: 200 }}
resizeMode="cover"
/>
// after
<Image
src="https://example.com/photo.jpg"
width={200}
height={200}
objectFit="cover"
/>Button
- Text style props (
fontFamily,fontSize, etc.) removed from the direct API -- style text through child components - Now defaults to
type="button"to prevent accidental form submissions useButtonhook is deprecated
ListItem
- Text style props removed -- use
ListItem.TextandListItem.Subtitlechild components - Internal spacing props (
spaceFlex,scaleSpace) removed
Tabs
activationModenow defaults to'manual'(was'automatic') -- users must click or press Enter to change tabs, matching web standardsTabs.Triggerdeprecated in favor ofTabs.Tab
Group
Group.Itemwrapper is now required (no more auto-cloning direct children)- Removed props:
space,separator,scrollable,showScrollIndicator,disablePassBorderRadius,forceUseItem - Add
<Separator />manually between items when needed
Sheet / Popover.Sheet
Popover.Sheet sub-components are replaced with standalone Sheet:
// before
<Adapt when="maxMd" platform="touch">
<Popover.Sheet modal dismissOnSnapToBottom>
<Popover.Sheet.Frame p="$4">
<Adapt.Contents />
</Popover.Sheet.Frame>
<Popover.Sheet.Overlay animation="quick" />
</Popover.Sheet>
</Adapt>
// after
<Adapt when="max-md" platform="touch">
<Sheet modal dismissOnSnapToBottom>
<Sheet.Frame p="$4">
<Adapt.Contents />
</Sheet.Frame>
<Sheet.Overlay transition="quick" />
</Sheet>
</Adapt>Select
SelectLabel is now SizableText-based instead of ListItem-based.
Removed APIs
<Spacer />removed from core -- import from@hanzogui/spacercomposeEventHandlers-- compose manually:(val) => { a(val); b?.(val) }useTheme(props)-- use<Theme>component insteadThemeableStack-- useViewbackgroundedprop -- usebg="$background"selectableprop -- useselect="text"animatePresenceprop (inline) -- wrap with<AnimatePresence>componentscrollbarWidthprop -- use CSSisWindowDefined-- useisBrowserfrom@hanzogui/constantsuseThemefrom@hanzogui/next-theme-- renamed touseThemeSetting@hanzogui/react-native-use-responder-events-- use pointer events (onPointerDown, etc.)
Native Setup
v2 requires explicit setup imports for native features. Add these at the top of your app entry before any Hanzo GUI imports. Note these are optional and mostly new, so only if you were using native gradient, or toast before do you need to do this.
// portals (Sheet, Dialog, Popover, Select, Toast)
import '@hanzogui/native/setup-teleport'
// LinearGradient
import '@hanzogui/native/setup-expo-linear-gradient'
// Toast (burnt)
import '@hanzogui/native/setup-burnt'
// Menu (zeego)
import '@hanzogui/native/setup-zeego'
// for smoother Sheet on native:
import '@hanzogui/native/setup-gesture-handler'Expo Router entry point
With Expo Router, these setup imports must run before expo-router/entry. Create a custom entry point:
// index.js (at project root)
import '@hanzogui/native/setup-zeego'
// add other setup imports here as needed
import 'expo-router/entry'Then update your package.json:
{
"main": "index.js"
}This ensures native modules like zeego are configured before Expo Router initializes your app.
Build Config
Not much has changed, but there's some nice improvements you can make:
Vite
import { guiAliases, guiPlugin } from '@hanzogui/vite-plugin'
export default {
plugins: [guiPlugin()],
resolve: {
alias: [
...guiAliases({
rnwLite: true, // use lightweight react-native-web
svg: true,
}),
],
},
}Create a gui.build.ts at your project root:
import type { GuiBuildOptions } from '@hanzo/gui'
export default {
components: ['@hanzo/gui'],
config: './src/gui.config.ts',
outputCSS: './src/gui.generated.css',
} satisfies GuiBuildOptionsMetro (Expo / React Native)
No special Metro configuration is needed for Hanzo GUI v2. A standard Expo Metro config works out of the box:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const config = getDefaultConfig(__dirname)
module.exports = configNext.js (Turbopack)
Add resolveExtensions and a react-native-safe-area-context shim:
// next.config.js
module.exports = {
turbopack: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@hanzogui/react-native-svg',
'react-native-safe-area-context': './shims/react-native-safe-area-context.js',
},
resolveExtensions: [
'.web.tsx',
'.web.ts',
'.web.js',
'.web.jsx',
'.tsx',
'.ts',
'.js',
'.jsx',
'.json',
],
},
}CSS
We recommend generating Hanzo GUI CSS to gui.generated.css, just so it's easier to configure linters and such:
// before
import './gui.css'
// after
import './gui.generated.css'Generate it with: npx hanzo-gui generate-css --output ./src/gui.generated.css
Quick Reference: Find-and-Replace Patterns
These are the most common replacements you can do across your codebase. Run carefully and review each change:
animation= -> transition=
AnimationProp -> TransitionProp
AnimationKeys -> TransitionKeys
tag= -> render=
themeInverse -> theme="accent"
<Theme inverse -> <Theme name="accent"
<Stack -> <View
</Stack> -> </View>
StackProps -> ViewProps
onHoverIn= -> onPointerEnter=
onHoverOut= -> onPointerLeave=
$2xl= -> $xxl=
$2xs= -> $xxs=
maxMD -> max-md
maxLG -> max-lg
maxSM -> max-sm
maxXL -> max-xl
max2Xl -> max-xxlNew Features Worth Knowing
While not required for migration, these v2 features can improve your app:
boxShadow-- full CSS box-shadow support with tokensbackgroundImage-- CSS gradients with token supportfilter,mixBlendMode-- graphical effectsscopeprop on Dialog, Popover, Sheet, Tooltip -- mount once at root for performanceactiveStyle/activeThemeon Switch, Checkbox, ToggleGroup, Tabs- Multiple animation drivers via
animatedByprop transitiondelay and enter/exit control --transition={{ enter: 'lazy', exit: 'quick' }}- Menu and ContextMenu -- new components with native platform rendering
- Headless components --
@hanzogui/switch-headless,@hanzogui/checkbox-headless, etc. - CLI commands --
gui build,gui generate,gui generate-prompt,gui check - ~32% smaller core bundle (37KB to 25KB gzipped)
Migration Checklist
- Upgrade React 19+, React Native 0.81+, TypeScript 5+
- Bump all
@hanzogui/*packages to^2.0.0 - Add package resolutions for
@hanzogui/core,@hanzogui/web,@hanzo/gui - Update config from
@hanzogui/config/v4to@hanzogui/config/v5 - Import animations separately (
@hanzogui/config/v5-cssor other driver) - Add
declare module '@hanzo/gui'type augmentation - Move root-level settings into
settingsobject - Rename
animationprop totransitioneverywhere - Rename
tagprop torender - Replace
StackwithView,StackPropswithViewProps - Replace
themeInverse/<Theme inverse>withtheme="accent" - Replace
space/spaceDirectionwithgap - Replace
onHoverIn/onHoverOutwith pointer events - Migrate shadows to
boxShadow - Update media query names (
$2xlto$xxl, max queries to kebab-case) - Update accessibility props to ARIA equivalents
- Update Input props to web-standard API
- Update Image from
sourcetosrc - Wrap Group children in
Group.Item - Update Tabs (
activationModedefault changed,TriggertoTab) - Replace
Popover.Sheet.*with standaloneSheet.* - Update Toast from
ToastProviderwrapper toToastersibling - Import
Spacerfrom@hanzogui/spacerif used - Replace
@hanzogui/animations-motiwith@hanzogui/animations-reanimated - Add native setup imports (portals, LinearGradient, Toast, Menu)
- Review flex/position defaults (add
position="relative"where needed) - Update build config (Metro package exports, Vite aliases, CSS generation)
- Update snapshot tests (shadow colors now use
color-mix()) - Run
npx hanzo-gui checkto verify dependency consistency
Last updated on