Files
zeekay c183da5389 Drop webpack from the stack
The loader and the Next plugin only existed to drive webpack; Next runs
on Turbopack and Vite has its own plugin. kitchen-sink and the
integration app serve through vite, the optimizer suite drops the
webpack name, and its fixture packages join the workspace so a fresh
checkout links @my/ui.
2026-09-06 08:17:57 -07:00

699 lines
18 KiB
Plaintext

---
title: Next.js Guide
description: How to set up Gui with Next.js
---
<Notice theme="green">
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.
</Notice>
Create a new [Next.js](https://nextjs.org/docs/getting-started/installation)
project:
```bash
npx create-next-app@latest
```
We recommend starting with our default config which gives you media queries and
other nice things:
```tsx fileName="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:
```bash
yarn add -D @hanzogui/cli
```
Create `hanzogui.build.ts`:
```ts fileName="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:
```ts fileName="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
```
<Notice theme="blue">
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.
</Notice>
### Build Scripts
The CLI can wrap your build command, optimizing files beforehand and restoring
them after:
```json fileName="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:
```bash
# 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:
```tsx fileName="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>
)
}
```
<Notice theme="blue">
With React 19, Gui automatically injects runtime styles via style tags. The `outputCSS`
file handles themes and tokens that are generated at build time.
</Notice>
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:
```json
{
"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.
```bash
yarn add @hanzogui/next-theme
```
Here's how to set up your `NextGuiProvider.tsx`:
```tsx fileName="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`:
```tsx fileName="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.
```bash
yarn add @hanzogui/next-theme
```
<PropsTable
data={[
{
name: 'skipNextHead',
required: false,
type: 'boolean',
description: `Required in app router. The internal usage of next/head is not supported in the app directory, so you need to add it.`,
},
{
name: 'enableSystem',
required: false,
type: 'boolean',
description: `Whether to switch between dark and light themes based on prefers-color-scheme.`,
},
{
name: 'defaultTheme',
required: false,
type: 'string',
description:
'If enableSystem is `false`, the default theme is light. Default theme name (for v0.0.12 and lower the default was light).',
},
{
name: 'forcedTheme',
required: false,
type: 'string',
description: 'Forced theme name for the current page.',
},
{
name: 'onChangeTheme',
required: false,
type: '(name: string) => void',
description:
'Used to change the current theme. The function receives the theme name as a parameter.',
},
{
name: 'systemTheme',
required: false,
type: 'string',
description: 'System theme name for the current page.',
},
{
name: 'enableColorScheme',
required: false,
type: 'boolean',
description: `Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons.`,
},
{
name: 'disableTransitionOnChange',
required: false,
type: 'boolean',
description: `Disable all CSS transitions when switching themes.`,
},
{
name: 'storageKey',
required: false,
type: 'string',
description: `Key used to store theme setting in localStorage.`,
},
{
name: 'themes',
required: false,
type: 'string[]',
description: `List of all available theme names.`,
},
{
name: 'value',
required: false,
type: 'ValueObject',
description: `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`.
```tsx fileName="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](#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`:
```tsx fileName="_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>
)
}
}
```
<Notice theme="blue">
Gui automatically injects styles at runtime. You can optionally generate a static CSS
file - see the [Static CSS Output](#static-css-output) section.
</Notice>
### pages/\_app.tsx
Add `GuiProvider`:
```tsx fileName="_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>
</>
)
}
```
<Notice theme="blue">
Use `disableInjectCSS` for SSR apps to prevent duplicate style injection. Only omit it
for client-only apps without server rendering.
</Notice>
### 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.
```bash
yarn add @hanzogui/next-theme
```
Here's how to set up your `_app.tsx`:
```tsx fileName="_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:
```bash
npx hanzogui generate
```
This outputs CSS to `.hanzogui/hanzogui.generated.css`. Copy it to your public folder or
configure `outputCSS` in your `hanzogui.build.ts`:
```ts fileName="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`:
```tsx fileName="_app.tsx"
import '../public/hanzogui.generated.css'
```
<Notice theme="green">
With `outputCSS`, you don't need `getCSS()` in your `_document.tsx` - all styles are
handled by the static CSS file and runtime style injection.
</Notice>
## App Router
Gui includes Server Components support for the Next.js app directory with
[`use client`](https://nextjs.org/docs/app/building-your-application/rendering/client-components#using-client-components-in-nextjs)
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](#setup) is all the configuration needed.
### app/layout.tsx
Create a new component to add `GuiProvider`:
<Notice theme="blue">
The internal usage of `next/head` is not supported in the app directory, so you need to add the `skipNextHead` prop to your `<NextThemeProvider>`.
</Notice>
```tsx fileName="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>
)
}
```
<Notice theme="green">
The `getNewCSS` helper in Gui will keep track of the last call and only return new
styles generated since the last usage.
</Notice>
Then add it to your `app/layout.tsx`:
```tsx fileName="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>
)
}
```
<Notice theme="green">
You can use `suppressHydrationWarning` to avoid the warning about mismatched content
during hydration in dev mode.
</Notice>
### app/page.tsx
Now you're ready to start adding components to `app/page.tsx`:
```tsx fileName="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.
```bash
yarn add @hanzogui/next-theme
```
Here's how to set up your `NextGuiProvider.tsx`:
```tsx fileName="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](#setup)), `hanzogui build`
and `npx hanzogui generate` both write the file. Import it in your `app/layout.tsx`:
```tsx fileName="app/layout.tsx"
import '../public/hanzogui.generated.css'
```
<Notice theme="green">
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.
</Notice>