Next.js Guide
How to set up Hanzo GUI with Next.js
Running npm create hanzo-gui@latest lets you choose the starter-free starter which is
a nicely configured Next.js app where you can take or leave whatever you want.
Create a new Next.js project:
npx create-next-app@latestWe recommend starting with our default config which gives you media queries and other nice things:
import { defaultConfig } from '@hanzogui/config/v5'
import { createGui } from '@hanzo/gui' // or '@hanzogui/core'
const appConfig = createGui(defaultConfig)
export type AppConfig = typeof appConfig
declare module '@hanzo/gui' {
// or '@hanzogui/core'
// overrides GuiCustomConfig so your custom types
// work everywhere you import `@hanzo/gui`
interface GuiCustomConfig extends AppConfig {}
}
export default appConfigSetup
Next.js uses Turbopack by default. In dev mode, Hanzo GUI works without any setup. For production builds, use the Hanzo GUI CLI to optimize your build.
Install the CLI:
yarn add -D @hanzogui/cliCreate gui.build.ts:
import type { GuiBuildOptions } from '@hanzogui/core'
export default {
components: ['@hanzogui/core'], // or ['@hanzo/gui']
config: './gui.config.ts',
outputCSS: './public/gui.generated.css',
} satisfies GuiBuildOptionsYour next.config.ts needs some configuration:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// some Hanzo GUI packages may need transpiling
transpilePackages: ['@hanzogui/lucide-icons-2'],
experimental: {
turbo: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@hanzogui/react-native-svg',
},
},
},
}
export default nextConfigThe transpilePackages array may need additional packages depending on which Hanzo GUI
packages you use. If you see module resolution errors, try adding the problematic
package to this array.
Build Scripts
The CLI can wrap your build command, optimizing files beforehand and restoring them after:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "gui build --target web ./src -- next build"
}
}The -- separator tells the CLI to run next build after optimization, then
restore your source files automatically.
You can also target specific files or use --include/--exclude patterns:
# Target specific files
gui build --target web ./src/components/Button.tsx ./src/components/Card.tsx -- next build
# Use glob patterns
gui build --target web --include "src/components/**/*.tsx" --exclude "src/components/**/*.test.tsx" ./src -- next buildCSS Setup
The CLI generates theme CSS to outputCSS. Commit this file to git and import
it in your layout:
import '../public/gui.generated.css'
import { GuiProvider } from '@hanzogui/core'
import config from '../gui.config'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GuiProvider config={config}>{children}</GuiProvider>
</body>
</html>
)
}With React 19, Hanzo GUI automatically injects runtime styles via style tags. The
outputCSS file handles themes and tokens that are generated at build time.
Run npx hanzo-gui build once to generate the initial CSS file, then commit it.
CI Verification
Use --expect-optimizations to fail builds if the compiler optimizes fewer than
the expected minimum number of components:
{
"build": "gui build --target web --expect-optimizations 5 ./src -- next build"
}This will fail the build if fewer than 5 components are optimized, helping catch configuration issues in CI.
Themes
We've created a package called @hanzogui/next-theme that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named light and dark, but you can override this. This is
pre-configured in the create-hanzo-gui starter.
yarn add @hanzogui/next-themeHere's how to set up your NextGuiProvider.tsx:
'use client'
import { ReactNode } from 'react'
import { NextThemeProvider, useRootTheme } from '@hanzogui/next-theme'
import { GuiProvider } from '@hanzo/gui'
import guiConfig from '../gui.config'
export const NextGuiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
return (
<NextThemeProvider
skipNextHead
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<GuiProvider config={guiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</GuiProvider>
</NextThemeProvider>
)
}Then update your app/layout.tsx:
import '../public/gui.generated.css'
import { Metadata } from 'next'
import { NextGuiProvider } from './NextGuiProvider'
export const metadata: Metadata = {
title: 'Your page title',
description: 'Your page description',
icons: '/favicon.ico',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NextGuiProvider>{children}</NextGuiProvider>
</body>
</html>
)
}NextThemeProvider
NextThemeProvider lets you set the theme for your app and provides a hook to
access the current theme and toggle between themes.
yarn add @hanzogui/next-theme| Prop | Type | Default | Required |
|---|---|---|---|
| skipNextHead | boolean | - | - |
| enableSystem | boolean | - | - |
| defaultTheme | string | - | - |
| forcedTheme | string | - | - |
| onChangeTheme | (name: string) => void | - | - |
| systemTheme | string | - | - |
| enableColorScheme | boolean | - | - |
| disableTransitionOnChange | boolean | - | - |
| storageKey | string | - | - |
| themes | string[] | - | - |
| value | ValueObject | - | - |
Theme toggle
If you need to access the current theme, say for a toggle button, you will then
use the useThemeSetting hook. We'll release an update in the future that makes
this automatically work better with Hanzo GUI's built-in useThemeSetting.
import { useState } from 'react'
import { Button, useIsomorphicLayoutEffect } from '@hanzo/gui'
import { useThemeSetting, useRootTheme } from '@hanzogui/next-theme'
export const SwitchThemeButton = () => {
const themeSetting = useThemeSetting()
const [theme] = useRootTheme()
const [clientTheme, setClientTheme] = useState<string | undefined>('light')
useIsomorphicLayoutEffect(() => {
setClientTheme(themeSetting.forcedTheme || themeSetting.current || theme)
}, [themeSetting.current, themeSetting.resolvedTheme])
return <Button onPress={themeSetting.toggle}>Change theme: {clientTheme}</Button>
}For Older Versions
The following sections cover setup for older Next.js versions using Webpack instead of Turbopack.
Webpack Plugin
If you aren't using Turbopack, you may want the optional @hanzogui/next-plugin,
which smooths out a few settings. See the
compiler install docs for more options.
Add @hanzogui/next-plugin to your project:
yarn add @hanzogui/next-pluginPages Router
next.config.js
Set up the optional Hanzo GUI plugin in next.config.js:
const { withGui } = require('@hanzogui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const guiPlugin = withGui({
config: './gui.config.ts',
components: ['@hanzo/gui'],
})
return {
...config,
...guiPlugin(config),
}
}pages/_document.tsx
If you're using React Native Web components, gather the react-native-web
styles in _document.tsx:
import NextDocument, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document'
import { StyleSheet } from 'react-native'
export default class Document extends NextDocument {
static async getInitialProps({ renderPage }: DocumentContext) {
const page = await renderPage()
// @ts-ignore RN doesn't have this type
const rnwStyle = StyleSheet.getSheet()
return {
...page,
styles: (
<style
id={rnwStyle.id}
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
/>
),
}
}
render() {
return (
<Html lang="en">
<Head>
<meta id="theme-color" name="theme-color" />
<meta name="color-scheme" content="light dark" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}Hanzo GUI automatically injects styles at runtime. You can optionally generate a static CSS file - see the Static CSS Output section.
pages/_app.tsx
Add GuiProvider:
import { NextThemeProvider } from '@hanzogui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { GuiProvider } from '@hanzo/gui'
import guiConfig from '../gui.config'
export default function App({ Component, pageProps }: AppProps) {
// memo to avoid re-render on dark/light change
const contents = useMemo(() => {
return <Component {...pageProps} />
}, [pageProps])
return (
<>
<Head>
<title>Your page title</title>
<meta name="description" content="Your page description" />
<link rel="icon" href="/favicon.ico" />
</Head>
<NextThemeProvider>
<GuiProvider config={guiConfig} disableInjectCSS disableRootThemeClass>
{contents}
</GuiProvider>
</NextThemeProvider>
</>
)
}Use disableInjectCSS for SSR apps to prevent duplicate style injection. Only omit it
for client-only apps without server rendering.
Themes (Pages Router)
We've created a package called @hanzogui/next-theme that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named light and dark, but you can override this. This is
pre-configured in the create-hanzo-gui starter.
yarn add @hanzogui/next-themeHere's how to set up your _app.tsx:
import { NextThemeProvider, useRootTheme } from '@hanzogui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { GuiProvider, createGui } from '@hanzo/gui'
// you usually export this from a gui.config.ts file:
import { defaultConfig } from '@hanzogui/config/v5'
const guiConfig = createGui(defaultConfig)
// make TypeScript type everything based on your config
type Conf = typeof guiConfig
declare module '@hanzogui/core' {
interface GuiCustomConfig extends Conf {}
}
export default function App({ Component, pageProps }: AppProps) {
const [theme, setTheme] = useRootTheme()
// memo to avoid re-render on dark/light change
const contents = useMemo(() => {
return <Component {...pageProps} />
}, [pageProps])
return (
<>
<Head>
<title>Your page title</title>
<meta name="description" content="Your page description" />
<link rel="icon" href="/favicon.ico" />
</Head>
<NextThemeProvider
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={setTheme as any}
>
<GuiProvider
config={guiConfig}
disableInjectCSS
disableRootThemeClass
defaultTheme={theme}
>
{contents}
</GuiProvider>
</NextThemeProvider>
</>
)
}Static CSS Output (Pages Router)
You can generate a static CSS file for your themes and tokens. There are two ways to do this:
Option 1: Using the CLI
The simplest approach is to use the Hanzo GUI CLI to generate the CSS file:
npx hanzo-gui generateThis outputs CSS to .hanzoai/gui.generated.css. Copy it to your public folder or
configure outputCSS in your gui.build.ts:
import type { GuiBuildOptions } from '@hanzogui/core'
export default {
components: ['@hanzo/gui'],
config: './gui.config.ts',
outputCSS: './public/gui.generated.css',
} satisfies GuiBuildOptionsThen import it in your _app.tsx:
import '../public/gui.generated.css'Option 2: Using the Next.js Plugin
You can also have the plugin generate CSS during your Next.js build:
const guiPlugin = withGui({
config: './gui.config.ts',
components: ['@hanzo/gui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/gui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})Then import it in your _app.tsx:
import '../public/gui.generated.css'With outputCSS, you don't need getCSS() in your _document.tsx - all styles are
handled by the static CSS file and runtime style injection.
App Router (Webpack)
Hanzo GUI includes Server Components support for the Next.js app directory with
use client
support.
Note that "use client" components do render on the server, and since Hanzo GUI
extracts to CSS statically and uses inline <style /> tags for non-static
styling, you get excellent performance out of the box.
next.config.js
The Hanzo GUI plugin is optional but helps with compatibility with the rest of the
React Native ecosystem. It requires CommonJS for now because the optimizing
compiler uses various resolving features that haven't been ported to ESM yet.
Rename your next.config.mjs to next.config.js before adding it:
const { withGui } = require('@hanzogui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const guiPlugin = withGui({
config: './gui.config.ts',
components: ['@hanzo/gui'],
appDir: true,
})
return {
...config,
...guiPlugin(config),
}
}You need to pass the appDir boolean to @hanzogui/next-plugin.
app/layout.tsx
Create a new component to add GuiProvider:
The internal usage of next/head is not supported in the app directory, so you need to add the skipNextHead prop to your <NextThemeProvider>.
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider } from '@hanzogui/next-theme'
import { GuiProvider } from '@hanzo/gui'
import guiConfig from '../gui.config'
export const NextGuiProvider = ({ children }: { children: ReactNode }) => {
// only if using react-native-web components like ScrollView:
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<>
<style
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
id={rnwStyle.id}
/>
</>
)
})
return (
<NextThemeProvider skipNextHead>
<GuiProvider config={guiConfig} disableRootThemeClass>
{children}
</GuiProvider>
</NextThemeProvider>
)
}The getNewCSS helper in Hanzo GUI will keep track of the last call and only return new
styles generated since the last usage.
Then add it to your app/layout.tsx:
import { Metadata } from 'next'
import { NextGuiProvider } from './NextGuiProvider'
export const metadata: Metadata = {
title: 'Your page title',
description: 'Your page description',
icons: '/favicon.ico',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NextGuiProvider>{children}</NextGuiProvider>
</body>
</html>
)
}You can use suppressHydrationWarning to avoid the warning about mismatched content
during hydration in dev mode.
app/page.tsx
Now you're ready to start adding components to app/page.tsx:
'use client'
import { Button } from '@hanzo/gui'
export default function Home() {
return <Button>Hello world!</Button>
}Themes (App Router Webpack)
We've created a package called @hanzogui/next-theme that properly supports SSR
light/dark themes while respecting user system preferences. It assumes your
themes are named light and dark, but you can override this. This is
pre-configured in the create-hanzo-gui starter.
yarn add @hanzogui/next-themeHere's how to set up your NextGuiProvider.tsx:
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider, useRootTheme } from '@hanzogui/next-theme'
import { GuiProvider } from '@hanzo/gui'
import guiConfig from '../gui.config'
export const NextGuiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
// only needed if using react-native-web components:
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<style
dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }}
id={rnwStyle.id}
/>
)
})
return (
<NextThemeProvider
skipNextHead
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<GuiProvider config={guiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</GuiProvider>
</NextThemeProvider>
)
}Static CSS Output (App Router Webpack)
You can generate a static CSS file for your themes and tokens. Use either the CLI or the Next.js plugin:
const guiPlugin = withGui({
config: './gui.config.ts',
components: ['@hanzo/gui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/gui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})Then link the generated CSS file in your app/layout.tsx:
import '../public/gui.generated.css'With React 19, Hanzo GUI automatically injects runtime styles via style tags on the
server. The outputCSS file handles themes and tokens generated at build time, so you
don't need any getCSS() calls in your provider.
Last updated on