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 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.
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 fileimport { 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 typeconst 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 browsersimport '@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 fileimport { config } 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></>)}
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 browsersimport '@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 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></>)}
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-ignoreconst 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-ignoreconst 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