mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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.
699 lines
18 KiB
Plaintext
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>
|