Next.js Guide
How to set up Tamagui with Next.js
Running npm create tamagui@latest let's you choose the starter-free
starter which is a very 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
If you are using Turbopack, we have a section for optimization at the end of
this document. If you aren't, then you may want the optional
@tamagui/next-plugin which helps smooth out a few settings. We'll show how to
configure it for both pages and app router in this guide. See the
compiler install docs for more options.
Add @tamagui/next-plugin to your project:
yarn add @tamagui/next-plugin
We recommend starting with our default config which gives you media queries and other nice things:
tamagui.config.ts
import { defaultConfig } from '@tamagui/config/v4'import { createTamagui } from 'tamagui' // or '@tamagui/core'const appConfig = createTamagui(defaultConfig)export type AppConfig = typeof appConfigdeclare module 'tamagui' {// or '@tamagui/core'// overrides TamaguiCustomConfig so your custom types// work everywhere you import `tamagui`interface TamaguiCustomConfig extends AppConfig {}}export default appConfig
From here, choose your Next.js routing option to continue:
Pages router
Automatically generate routes based on the filenames.
App router
Allows more complex patterns and setups.
Turbopack
Using Tamagui with Next.js Turbopack bundler.
Pages router
next.config.js
Set up the optional Tamagui plugin to next.config.js:
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),}}
pages/_document.tsx
If you're using React Native Web components, you'll want to gather the
react-native-web styles in _document:
_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 typeconst 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>)}}
Tamagui automatically injects styles at runtime. You can optionally generate a static CSS file - see the Static CSS Output section.
pages/_app.tsx
Add TamaguiProvider:
_app.tsx
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 changeconst 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></>)}
Use disableInjectCSS for SSR apps to prevent duplicate style injection. Only omit it for client-only apps without server rendering.
Themes
We've created a package that works with Tamagui to properly support SSR
light/dark themes that also respect user system preference, called
@tamagui/next-theme. It assumes your light/dark themes are named as such,
but you can override it. This is pre-configured in the create-tamagui starter.
yarn add @tamagui/next-theme
Here's how you'd set up your _app.tsx:
_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/v4'const tamaguiConfig = createTamagui(defaultConfig)// make TypeScript type everything based on your configtype Conf = typeof tamaguiConfigdeclare 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 changeconst 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></>)}
Static CSS Output
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 Tamagui CLI to generate the CSS file:
yarn dlx tamagui generate
This outputs CSS to .tamagui/tamagui.css. Copy it to your public folder or
configure outputCSS in your tamagui.build.ts:
tamagui.build.ts
import type { TamaguiBuildOptions } from '@tamagui/core'export default {components: ['tamagui'],config: './tamagui.config.ts',outputCSS: './public/tamagui.css',} satisfies TamaguiBuildOptions
Then import it in your _app.tsx:
_app.tsx
import '../public/tamagui.css'
Option 2: Using the Next.js Plugin
You can also have the plugin generate CSS during your Next.js build:
next.config.js
const tamaguiPlugin = withTamagui({config: './tamagui.config.ts',components: ['tamagui'],outputCSS:process.env.NODE_ENV === 'production' ? './public/tamagui.css' : null,// faster dev mode, keeps debugging helpers:disableExtraction: process.env.NODE_ENV === 'development',})
Then import it in your _app.tsx:
_app.tsx
import '../public/tamagui.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.
Font loading
To ensure font loads globally, add a global style to styles in
_document_.tsx:
NextTamaguiProvider.tsx
<style jsx global>{`html {font-family: 'Inter';}`}</style>
App router
Tamagui includes Server Components support for the Next.js app directory with
use clientÂ
support.
Note that "use client" does render on the server, and since Tamagui extracts to
CSS statically and uses inline <style /> tags for non-static styling, we get
excellent performance as-is.
next.config.js
The Tamagui plugin is optional but helps with compatibility with the rest of the
React Native ecosystem. It requires CommonJS for now as the optimizing compiler
makes use of a variety of resolving features that haven't been ported to ESM
yet. Be sure to rename your next.config.mjs to next.config.js before adding
it:
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'],appDir: true,})return {...config,...tamaguiPlugin(config),}}
You need to pass the appDir boolean to @tamagui/next-plugin.
app/layout.tsx
Create a new component to add TamaguiProvider:
The internal usage of next/head is not supported in the app directory, so you need to add the skipNextHead prop to your <NextThemeProvider>.
NextTamaguiProvider.tsx
'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-ignoreconst rnwStyle = StyleSheet.getSheet()return (<><style dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }} id={rnwStyle.id} /></>)})return (<NextThemeProvider skipNextHead><TamaguiProvider config={tamaguiConfig} disableRootThemeClass>{children}</TamaguiProvider></NextThemeProvider>)}
The getNewCSS helper in Tamagui 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 { 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>)}
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 'tamagui'export default function Home() {return <Button>Hello world!</Button>}
Themes
We've created a package that works with Tamagui to properly support SSR
light/dark themes that also respect user system preference, called
@tamagui/next-theme. It assumes your light/dark themes are named as such,
but you can override it. This is pre-configured in the create-tamagui starter.
yarn add @tamagui/next-theme
Here's how you'd set up your NextTamaguiProvider.tsx:
NextTamaguiProvider.tsx
'use client'import '@tamagui/font-inter/css/400.css'import '@tamagui/font-inter/css/700.css'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-ignoreconst 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>)}
Static CSS Output
You can generate a static CSS file for your themes and tokens. Use either the CLI or the Next.js plugin:
next.config.js
const tamaguiPlugin = withTamagui({config: './tamagui.config.ts',components: ['tamagui'],outputCSS:process.env.NODE_ENV === 'production' ? './public/tamagui.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:
app/layout.tsx
import '../public/tamagui.css'
With React 19, Tamagui 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.
Font loading
To ensure font loads globally, add a global style to useServerInsertedHTML in
NextTamaguiProvider.tsx:
NextTamaguiProvider.tsx
<style jsx global>{`html {font-family: 'Inter';}`}</style>
NextThemeProvider
The NextThemeProvider is a provider that allows you to set the theme for your
app. It also provides a hook to access the current theme and a function to
change the 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 Tamagui's built-in useThemeSetting.
SwitchThemeButton.tsx
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>)}
Turbopack
Turbopack doesn't support Webpack plugins, so you'll use the Tamagui CLI to optimize your build. In dev mode, Tamagui works without any setup.
Setup
Install the CLI:
yarn add -D @tamagui/cli
Create tamagui.build.ts:
tamagui.build.ts
import type { TamaguiBuildOptions } from '@tamagui/core'export default {components: ['@tamagui/core'], // or ['tamagui']config: './tamagui.config.ts',outputCSS: './public/tamagui.css',} satisfies TamaguiBuildOptions
Your next.config.js can be empty:
next.config.js
module.exports = {}
Build Scripts
The CLI can wrap your build command, optimizing files beforehand and restoring them after:
package.json
{"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.
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/tamagui.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>)}
With React 19, Tamagui automatically injects runtime styles via style tags.
The outputCSS file handles themes and tokens that are generated at build
time.
Run npx tamagui 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": "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.