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
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
Choose Next.js routing option:
next.config.js
To configure Tamagui for Next.js, you'll want to add the @tamagui/next-plugin
and set it up in your next config. See the compiler install docs for more options.
Add @tamagui/next-plugin
to your project:
yarn add @tamagui/next-plugin
Then add the plugin to your 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),}}
Performance
Use outputCSS
for better performance by generating CSS styles in build-time.
Add outputCSS
to your next.config.js
:
const tamaguiPlugin = withTamagui({config: './tamagui.config.ts',components: ['tamagui'],outputCSS: process.env.NODE_ENV === 'production' ? './public/tamagui.css' : null,// optional advanced settings:// set to false if you never call addTheme or updateTheme})
We recommend only using this for production so you get reloading during dev mode.
pages/_app.tsx
Adding TamaguiProvider
// 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) {// memo to avoid re-render on dark/light changeconst contents = useMemo(() => {return <Component {...pageProps} />}, [pageProps])return (<NextThemeProvider><Head><script dangerouslySetInnerHTML={{ // avoid flash of animated things on enter: __html: `document.documentElement.classList.add('t_unmounted')`, }} /></Head><TamaguiProvider config={tamaguiConfig} disableInjectCSS disableRootThemeClass>{contents}</TamaguiProvider></NextThemeProvider>)}
Because custom getCSS()
is used in _document.tsx, disableInjectCSS
is added to TamaguiProvider
And then add this if you are using the outputCSS
option to include the CSS file generated at build-time:
if (process.env.NODE_ENV === 'production') {require('../public/tamagui.css')}
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:
import '@tamagui/core/reset.css'
You will have to enable isCSSEnabled
if you're using Metro as your web bundler. See Metro guide.
pages/_document.tsx
You'll want to set up a _document.tsx
and gather both the react-native-web
style object using AppRegistry, as well as Tamagui styles using tamaguiConfig.getCSS()
into the head element.
We are only importing react-native
here because if you use tamagui
views like Input or Image they rely on React Native Image/Input (and therefore on the web, react-native-web
). If you are just using core, or aren't using Image or Input in tamagui
, you can forego the entire AppRegistry
import and setup here. We're working on making even tamagui
completely de-coupled from RN/RNW by building their own image and input components eventually.
Configuring _document.tsx
import NextDocument, {DocumentContext,Head,Html,Main,NextScript,} from 'next/document'import { StyleSheet } from 'react-native'import { config } from '@tamagui/config/v3'import { createTamagui } from 'tamagui'const tamaguiConfig = createTamagui(config)// you usually export this from a tamagui.config.ts file:// import tamaguiConfig 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>)}}
Using outputCSS
If you are using outputCSS
option, you should add exclude
option to getCSS()
in _document.tsx
:
<head><style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getCSS({ exclude: process.env.NODE_ENV === 'production' ? 'design-system' : null, }), }} /></head>
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
:
// 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 (<NextThemeProvider onChangeTheme={setTheme as any}><Head><script dangerouslySetInnerHTML={{ // avoid flash of animated things on enter: __html: `document.documentElement.classList.add('t_unmounted')`, }} /></Head><TamaguiProvider config={tamaguiConfig} disableInjectCSS disableRootThemeClass defaultTheme={theme} >{contents}</TamaguiProvider></NextThemeProvider>)}
We recommend memo-ing children so they don't re-render.
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 { Button, useIsomorphicLayoutEffect } from 'tamagui'import { useThemeSetting } from '@tamagui/next-theme'import { useState } from 'react'export default function Home() {const themeSetting = useThemeSetting()const [clientTheme, setClientTheme] = useState<string>('dark')useIsomorphicLayoutEffect(() => {setClientTheme(themeSetting.current || 'dark')}, [themeSetting.current, themeSetting.resolvedTheme])return <Button onPress={themeSetting.toggle}>Change theme: {clientTheme}</Button>}
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 your _app.tsx render output:
<Head><script dangerouslySetInnerHTML={{ // avoid flash of entered elements before enter animations run: __html: `document.documentElement.classList.add('t_unmounted')`, }} /></Head>
Server components
Tamagui includes Server Components support for the Next.js app directory with use client
support. We're working on a mode with limitations that enables full server support.
Create a new component:
'use client'import { useServerInsertedHTML } from 'next/navigation'import { TamaguiProvider } 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'export function NextTamaguiProvider({ children }) {useServerInsertedHTML(() => {// 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 callreturn <style dangerouslySetInnerHTML={{ __html: tamaguiConfig.getNewCSS() }} />})// see Tamagui provider setup in the example abovereturn <TamaguiProvider>{children}</TamaguiProvider>}
The new getNewCSS
helper in Tamagui will keep track of the last call and only return new styles generated since the last usage.
And then use it in your app/layout.tsx
like this:
import { NextTamaguiProvider } from './NextTamaguiProvider'export default function Layout({ children }) {return <NextTamaguiProvider>{children}</NextTamaguiProvider>}
Additionally, you will need to add the skipNextHead
prop to your <NextThemeProvider>
, as the internal usage of next/head
is not supported in the app directory.
And you'll need to pass the appDir
boolean to @tamagui/next-plugin
:
withTamagui({appDir: true,})
Previous
Developing
Next
Expo