code/tamagui.dev/data/docs/guides/next-js.mdx
Create a new Next.js project:
npx create-next-app@latest
We recommend starting with our default config which gives you media queries and other nice things:
import { defaultConfig } from '@tamagui/config/v5'
import { createTamagui } from 'tamagui' // or '@tamagui/core'
const appConfig = createTamagui(defaultConfig)
export type AppConfig = typeof appConfig
declare module 'tamagui' {
// or '@tamagui/core'
// overrides TamaguiCustomConfig so your custom types
// work everywhere you import `tamagui`
interface TamaguiCustomConfig extends AppConfig {}
}
export default appConfig
Next.js uses Turbopack by default. In dev mode, Tamagui works without any setup. For production builds, use the Tamagui CLI to optimize your build.
Install the CLI:
yarn add -D @tamagui/cli
Create tamagui.build.ts:
import type { TamaguiBuildOptions } from '@tamagui/core'
export default {
components: ['@tamagui/core'], // or ['tamagui']
config: './tamagui.config.ts',
outputCSS: './public/tamagui.generated.css',
} satisfies TamaguiBuildOptions
Your next.config.ts needs some configuration:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// some Tamagui packages may need transpiling
transpilePackages: ['@tamagui/lucide-icons-2'],
experimental: {
turbo: {
resolveAlias: {
'react-native': 'react-native-web',
'react-native-svg': '@tamagui/react-native-svg',
},
},
},
}
export default nextConfig
The CLI can wrap your build command, optimizing files beforehand and restoring them after:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "tamagui 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
tamagui build --target web ./src/components/Button.tsx ./src/components/Card.tsx -- next build
# Use glob patterns
tamagui build --target web --include "src/components/**/*.tsx" --exclude "src/components/**/*.test.tsx" ./src -- next build
The CLI generates theme CSS to outputCSS. Commit this file to git and import
it in your layout:
import '../public/tamagui.generated.css'
import { TamaguiProvider } from '@tamagui/core'
import config from '../tamagui.config'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<TamaguiProvider config={config}>{children}</TamaguiProvider>
</body>
</html>
)
}
Run npx tamagui build once to generate the initial CSS file, then commit it.
Use --expect-optimizations to fail builds if the compiler optimizes fewer than
the expected minimum number of components:
{
"build": "tamagui 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.
We've created a package called @tamagui/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-tamagui starter.
yarn add @tamagui/next-theme
Here's how to set up your NextTamaguiProvider.tsx:
'use client'
import { ReactNode } from 'react'
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
return (
<NextThemeProvider
skipNextHead
// change default theme (system) here:
// defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
Then update your app/layout.tsx:
import '../public/tamagui.generated.css'
import { Metadata } from 'next'
import { NextTamaguiProvider } from './NextTamaguiProvider'
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>
<NextTamaguiProvider>{children}</NextTamaguiProvider>
</body>
</html>
)
}
NextThemeProvider lets you set the theme for your app and provides a hook to
access the current theme and toggle between themes.
yarn add @tamagui/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.,
},
]}
/>
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 Tamagui's built-in useThemeSetting.
import { useState } from 'react'
import { Button, useIsomorphicLayoutEffect } from 'tamagui'
import { useThemeSetting, useRootTheme } from '@tamagui/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>
}
The following sections cover setup for older Next.js versions using Webpack instead of Turbopack.
If you aren't using Turbopack, you may want the optional @tamagui/next-plugin,
which smooths out a few settings. See the
compiler install docs for more options.
Add @tamagui/next-plugin to your project:
yarn add @tamagui/next-plugin
Set up the optional Tamagui plugin in next.config.js:
const { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
})
return {
...config,
...tamaguiPlugin(config),
}
}
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>
)
}
}
Add TamaguiProvider:
import { NextThemeProvider } from '@tamagui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.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>
<TamaguiProvider config={tamaguiConfig} disableInjectCSS disableRootThemeClass>
{contents}
</TamaguiProvider>
</NextThemeProvider>
</>
)
}
We've created a package called @tamagui/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-tamagui starter.
yarn add @tamagui/next-theme
Here's how to set up your _app.tsx:
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { AppProps } from 'next/app'
import Head from 'next/head'
import React, { useMemo } from 'react'
import { TamaguiProvider, createTamagui } from 'tamagui'
// you usually export this from a tamagui.config.ts file:
import { defaultConfig } from '@tamagui/config/v5'
const tamaguiConfig = createTamagui(defaultConfig)
// make TypeScript type everything based on your config
type Conf = typeof tamaguiConfig
declare module '@tamagui/core' {
interface TamaguiCustomConfig 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}
>
<TamaguiProvider
config={tamaguiConfig}
disableInjectCSS
disableRootThemeClass
defaultTheme={theme}
>
{contents}
</TamaguiProvider>
</NextThemeProvider>
</>
)
}
You can generate a static CSS file for your themes and tokens. There are two ways to do this:
The simplest approach is to use the Tamagui CLI to generate the CSS file:
npx tamagui generate
This outputs CSS to .tamagui/tamagui.generated.css. Copy it to your public folder or
configure outputCSS in your tamagui.build.ts:
import type { TamaguiBuildOptions } from '@tamagui/core'
export default {
components: ['tamagui'],
config: './tamagui.config.ts',
outputCSS: './public/tamagui.generated.css',
} satisfies TamaguiBuildOptions
Then import it in your _app.tsx:
import '../public/tamagui.generated.css'
You can also have the plugin generate CSS during your Next.js build:
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})
Then import it in your _app.tsx:
import '../public/tamagui.generated.css'
Tamagui 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 Tamagui
extracts to CSS statically and uses inline <style /> tags for non-static
styling, you get excellent performance out of the box.
The Tamagui 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 { withTamagui } = require('@tamagui/next-plugin')
module.exports = function (name, { defaultConfig }) {
let config = {
...defaultConfig,
// ...your configuration
}
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
appDir: true,
})
return {
...config,
...tamaguiPlugin(config),
}
}
Create a new component to add TamaguiProvider:
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ 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>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
Then add it to your app/layout.tsx:
import { Metadata } from 'next'
import { NextTamaguiProvider } from './NextTamaguiProvider'
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>
<NextTamaguiProvider>{children}</NextTamaguiProvider>
</body>
</html>
)
}
Now you're ready to start adding components to app/page.tsx:
'use client'
import { Button } from 'tamagui'
export default function Home() {
return <Button>Hello world!</Button>
}
We've created a package called @tamagui/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-tamagui starter.
yarn add @tamagui/next-theme
Here's how to set up your NextTamaguiProvider.tsx:
'use client'
import { ReactNode } from 'react'
import { StyleSheet } from 'react-native'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { TamaguiProvider } from 'tamagui'
import tamaguiConfig from '../tamagui.config'
export const NextTamaguiProvider = ({ 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)
}}
>
<TamaguiProvider config={tamaguiConfig} disableRootThemeClass defaultTheme={theme}>
{children}
</TamaguiProvider>
</NextThemeProvider>
)
}
You can generate a static CSS file for your themes and tokens. Use either the CLI or the Next.js plugin:
const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.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/tamagui.generated.css'