Next.js Guide

How to set up Gui with Next.js

Running npm create hanzogui@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:

yarn dlx create-next-app@latest

We recommend starting with our default config which gives you media queries and other nice things:

hanzogui.config.ts

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 'hanzogui' {
// or '@hanzogui/core'
// overrides GuiCustomConfig so your custom types
// work everywhere you import `hanzogui`
interface GuiCustomConfig extends AppConfig {}
}
export default appConfig

Setup

Next.js uses Turbopack by default. In dev mode, Gui works without any setup. For production builds, use the Gui CLI to optimize your build.

Install the CLI:

yarn add -D @hanzogui/cli

Create hanzogui.build.ts:

hanzogui.build.ts

import type { GuiBuildOptions } from '@hanzogui/core'
export default {
components: ['@hanzogui/core'], // or ['hanzogui']
config: './hanzogui.config.ts',
outputCSS: './public/hanzogui.generated.css',
} satisfies GuiBuildOptions

Your next.config.ts needs some configuration:

next.config.ts

import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// some 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 nextConfig
The transpilePackages array may need additional packages depending on which 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:

package.json

{
"scripts": {
"dev": "next dev --turbopack",
"build": "hanzogui 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:

Terminal

# Target specific files
hanzogui build --target web ./src/components/Button.tsx ./src/components/Card.tsx -- next build
# Use glob patterns
hanzogui build --target web --include "src/components/**/*.tsx" --exclude "src/components/**/*.test.tsx" ./src -- next build

CSS Setup

The CLI generates theme CSS to outputCSS. Commit this file to git and import it in your layout:

app/layout.tsx

import '../public/hanzogui.generated.css'
import { GuiProvider } from '@hanzogui/core'
import config from '../hanzogui.config'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GuiProvider config={config}>{children}</GuiProvider>
</body>
</html>
)
}
With React 19, Gui automatically injects runtime styles via style tags. The outputCSS file handles themes and tokens that are generated at build time.

Run npx hanzogui 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": "hanzogui 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-gui starter.

yarn add @hanzogui/next-theme

Here’s how to set up your NextGuiProvider.tsx:

NextGuiProvider.tsx

'use client'
import { ReactNode } from 'react'
import { NextThemeProvider, useRootTheme } from '@hanzogui/next-theme'
import { GuiProvider } from '@hanzo/gui'
import hanzoguiConfig from '../hanzogui.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={hanzoguiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</GuiProvider>
</NextThemeProvider>
)
}

Then update your app/layout.tsx:

app/layout.tsx

import '../public/hanzogui.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

Props

  • skipNextHead

    boolean

    Required in app router. The internal usage of next/head is not supported in the app directory, so you need to add it.

  • enableSystem

    boolean

    Whether to switch between dark and light themes based on prefers-color-scheme.

  • defaultTheme

    string

    If enableSystem is `false`, the default theme is light. Default theme name (for v0.0.12 and lower the default was light).

  • forcedTheme

    string

    Forced theme name for the current page.

  • onChangeTheme

    (name: string) => void

    Used to change the current theme. The function receives the theme name as a parameter.

  • systemTheme

    string

    System theme name for the current page.

  • enableColorScheme

    boolean

    Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons.

  • disableTransitionOnChange

    boolean

    Disable all CSS transitions when switching themes.

  • storageKey

    string

    Key used to store theme setting in localStorage.

  • themes

    string[]

    List of all available theme names.

  • value

    ValueObject

    Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value.

  • 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 Gui’s built-in useThemeSetting.

    SwitchThemeButton.tsx

    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>
    }

    Pages Router

    The Setup above applies unchanged; what follows is the Pages Router side of the provider and theme code.

    pages/_document.tsx

    If you’re using React Native Web components, gather the react-native-web styles in _document.tsx:

    _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>
    )
    }
    }
    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:

    _app.tsx

    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 hanzoguiConfig from '../hanzogui.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={hanzoguiConfig} 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-gui starter.

    yarn add @hanzogui/next-theme

    Here’s how to set up your _app.tsx:

    _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 hanzogui.config.ts file:
    import { defaultConfig } from '@hanzogui/config/v5'
    const hanzoguiConfig = createGui(defaultConfig)
    // make TypeScript type everything based on your config
    type Conf = typeof hanzoguiConfig
    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={hanzoguiConfig} disableInjectCSS disableRootThemeClass defaultTheme={theme} >
    {contents}
    </GuiProvider>
    </NextThemeProvider>
    </>
    )
    }

    Static CSS Output (Pages Router)

    Generate a static CSS file for your themes and tokens with the CLI:

    yarn dlx hanzogui generate

    This outputs CSS to .hanzogui/hanzogui.generated.css. Copy it to your public folder or configure outputCSS in your hanzogui.build.ts:

    hanzogui.build.ts

    import type { GuiBuildOptions } from '@hanzogui/core'
    export default {
    components: ['@hanzo/gui'],
    config: './hanzogui.config.ts',
    outputCSS: './public/hanzogui.generated.css',
    } satisfies GuiBuildOptions

    Then import it in your _app.tsx:

    _app.tsx

    import '../public/hanzogui.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

    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 Gui extracts to CSS statically and uses inline <style /> tags for non-static styling, you get excellent performance out of the box.

    The next.config.ts from Setup is all the configuration needed.

    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>.

    NextGuiProvider.tsx

    '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 hanzoguiConfig from '../hanzogui.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={hanzoguiConfig} disableRootThemeClass>
    {children}
    </GuiProvider>
    </NextThemeProvider>
    )
    }
    The getNewCSS helper in 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:

    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:

    page.tsx

    'use client'
    import { Button } from '@hanzo/gui'
    export default function Home() {
    return <Button>Hello world!</Button>
    }

    Themes (App 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-gui starter.

    yarn add @hanzogui/next-theme

    Here’s how to set up your NextGuiProvider.tsx:

    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 hanzoguiConfig from '../hanzogui.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={hanzoguiConfig} disableRootThemeClass defaultTheme={theme}>
    {children}
    </GuiProvider>
    </NextThemeProvider>
    )
    }

    Static CSS Output (App Router)

    With outputCSS set in hanzogui.build.ts (see Setup), hanzogui build and npx hanzogui generate both write the file. Import it in your app/layout.tsx:

    app/layout.tsx

    import '../public/hanzogui.generated.css'
    With React 19, 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.