wwwwwwwwwwwwwwwwwww

Next.js Guide

How to set up Tamagui with Next.js

Check out the source for this site  to see a good example of a full featured Next.js website as well, especially the next.config.js and tamagui.config.ts.

Install

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

npx create-next-app@latest

For better compatibility with the React Native ecosystem, and better performance with the Tamagui compiler, add the optional @tamagui/next-plugin and set it up in your next config. 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

While you don't have to setup a config, we recommend starting with our default config which gives you media queries and other nice things:

tamagui.config.ts

import { config } from '@tamagui/config/v3'
import { Text, View } from 'react-native'
import { createTamagui } from 'tamagui' // or '@tamagui/core'
const appConfig = createTamagui(config)
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

From here, choose your Next.js routing option to continue:

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

You'll want modify _document to add the global Tamagui styles using tamaguiConfig.getCSS() into the head element. If you are also using React Native, we can gather the react-native-web style with AppRegistry here as well.

_document.tsx

import NextDocument, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document'
import { StyleSheet } from 'react-native'
// import the config you just exported from the tamagui.config.ts file
import { config } from './tamagui.config'
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 }} />
<style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getCSS(), }} />
</>
),
}
}
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>
)
}
}

pages/_app.tsx

Add TamaguiProvider:

tamagui.config.ts

// Optional: add the reset to get more consistent styles across browsers
import '@tamagui/core/reset.css'
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'
// import the config you just exported from the tamagui.config.ts file
import { config } 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>
</>
)
}

Because _document.tsx utilizes custom getCSS(), include disableInjectCSS in TamaguiProvider

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

// Optional: add the reset to get more consistent styles across browsers
import '@tamagui/core/reset.css'
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'
import { config } from '@tamagui/config/v3'
const tamaguiConfig = createTamagui(config)
// you usually export this from a tamagui.config.ts file:
// import tamaguiConfig from '../tamagui.config'
// 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>
</>
)
}

We recommend memo-ing children so they don't re-render.

Mount animations

Animations that run through JS drivers and have enterStyle set will want to add the following script that allows for hiding those animations before the JS runs. This is the "right way" to handle things, as it allows for disabling JS entirely and not accidentally hiding all unmounted things. Meanwhile, it still properly avoids a flash of mounted style for SSR pages.

Add this to <Head> in your _app.tsx:

_app.tsx

<script dangerouslySetInnerHTML={{ // avoid flash of entered elements before enter animations run: __html: `document.documentElement.classList.add('t_unmounted')`, }} />

Performance

Add outputCSS and disableExtraction to your next.config.js:

next.config.js

const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.css' : null,
disableExtraction: process.env.NODE_ENV === 'development"
})

We recommend using disableExtraction for better performance during dev mode. You still get the nice debugging helpers like file name, component name and line-numbers on every dom node.

And then add this to include the CSS file generated at build-time:

_app.tsx

if (process.env.NODE_ENV === 'production') {
require('../public/tamagui.css')
}

Add exclude option to getCSS() in _document.tsx:

_document.tsx

<head>
<style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getCSS({ exclude: process.env.NODE_ENV === 'production' ? 'design-system' : null, }), }} />
</head>

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>

Reseting CSS

There is an optional CSS reset to get more consistent styles across browsers, that helps normalize styling.

You can import it into your app like so:

_app.tsx

import '@tamagui/core/reset.css'

App router

Tamagui includes Server Components support for the Next.js app directory with use client  support. We're also actively working on a new mode that enables full server-side support with some limitations.

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 '@tamagui/core/reset.css'
import '@tamagui/polyfill-dev'
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 }) => {
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<>
<style dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }} id={rnwStyle.id} />
<style dangerouslySetInnerHTML={{ // the first time this runs you'll get the full CSS including all themes // after that, it will only return CSS generated since the last call __html: tamaguiConfig.getNewCSS(), }} />
</>
)
})
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/core/reset.css'
import '@tamagui/font-inter/css/400.css'
import '@tamagui/font-inter/css/700.css'
import '@tamagui/polyfill-dev'
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()
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<>
<style dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }} id={rnwStyle.id} />
<style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getCSS({ // if you are using "outputCSS" option, you should use this "exclude" // if not, then you can leave the option out exclude: process.env.NODE_ENV === 'production' ? 'design-system' : null, }), }} />
</>
)
})
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>
)
}

Mount animations

Animations that run through JS drivers and have enterStyle set will want to add the following script that allows for hiding those animations before the JS runs. This is the "right way" to handle things, as it allows for disabling JS entirely and not accidentally hiding all unmounted things. Meanwhile, it still properly avoids a flash of mounted style for SSR pages.

Add this to useServerInsertedHTML in your NextTamaguiProvider.tsx:

NextTamaguiProvider.tsx

<script dangerouslySetInnerHTML={{ // avoid flash of entered elements before enter animations run: __html: `document.documentElement.classList.add('t_unmounted')`, }} />

Performance

Add outputCSS and disableExtraction to your next.config.js:

next.config.js

const tamaguiPlugin = withTamagui({
config: './tamagui.config.ts',
components: ['tamagui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.css' : null,
disableExtraction: process.env.NODE_ENV === 'development"
})

We recommend using disableExtraction for better performance during dev mode. You still get the nice debugging helpers like file name, component name and line-numbers on every dom node.

Add exclude option to useServerInsertedHTML in NextTamaguiProvider:

NextTamaguiProvider.tsx

<style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getCSS({ // if you are using "outputCSS" option, you should use this "exclude" // if not, then you can leave the option out exclude: process.env.NODE_ENV === 'production' ? 'design-system' : null, }), }} />

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>

Reseting CSS

There is an optional CSS reset to get more consistent styles across browsers, that helps normalize styling.

You can import it into your app like so:

NextTamaguiProvider.tsx

import '@tamagui/core/reset.css'

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

    Previous

    Developing

    Next

    Expo