Skip to main content
Tamagui provides excellent server-side rendering (SSR) and static site generation (SSG) support with automatic CSS extraction and hydration. It works seamlessly with Next.js, Remix, Vite, and other frameworks.

How SSR Works

Tamagui’s SSR process:
  1. Server: Components render to HTML with inline styles
  2. CSS Extraction: Generated CSS is collected via getCSS()
  3. HTML Output: CSS is included in <head> as <style> tags
  4. Client Hydration: React hydrates without style flickering
  5. Runtime: Only dynamic styles are calculated client-side

Next.js Setup

App Router (Next.js 13+)

app/layout.tsx
import { TamaguiProvider } from 'tamagui'
import config from '../tamagui.config'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <TamaguiProvider config={config} defaultTheme="light">
          {children}
        </TamaguiProvider>
      </body>
    </html>
  )
}
app/head.tsx
import config from '../tamagui.config'

export default function Head() {
  return (
    <>
      <style
        dangerouslySetInnerHTML={{
          __html: config.getCSS(),
        }}
      />
    </>
  )
}

Pages Router (Next.js 12)

pages/_document.tsx
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
import { Children } from 'react'
import config from '../tamagui.config'

export default class Document extends NextDocument {
  static async getInitialProps({ renderPage }: any) {
    const page = await renderPage()
    
    // Get styles from components that rendered
    const styles = config.getNewCSS()
    
    return { ...page, styles }
  }
  
  render() {
    return (
      <Html>
        <Head>
          <style
            dangerouslySetInnerHTML={{
              __html: config.getCSS(),
            }}
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}
pages/_app.tsx
import type { AppProps } from 'next/app'
import { TamaguiProvider } from 'tamagui'
import config from '../tamagui.config'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <TamaguiProvider config={config} defaultTheme="light">
      <Component {...pageProps} />
    </TamaguiProvider>
  )
}

export default MyApp

Vite SSR

entry-server.tsx
import { renderToString } from 'react-dom/server'
import { TamaguiProvider } from 'tamagui'
import config from './tamagui.config'
import App from './App'

export function render() {
  // Render app
  const html = renderToString(
    <TamaguiProvider config={config} defaultTheme="light">
      <App />
    </TamaguiProvider>
  )
  
  // Get generated CSS
  const css = config.getCSS()
  
  return { html, css }
}
entry-client.tsx
import { hydrateRoot } from 'react-dom/client'
import { TamaguiProvider } from 'tamagui'
import config from './tamagui.config'
import App from './App'

hydrateRoot(
  document.getElementById('root')!,
  <TamaguiProvider config={config} defaultTheme="light">
    <App />
  </TamaguiProvider>
)

CSS Extraction

getCSS()

Get all generated CSS:
const css = config.getCSS()
Options:
const css = config.getCSS({
  // Exclude design system CSS (tokens, themes, fonts)
  exclude: 'design-system',
  
  // Exclude specific groups
  exclude: ['themes', 'tokens'],
})

getNewCSS()

Get only CSS generated since last call:
// First call: returns all CSS
const initialCSS = config.getNewCSS()

// Render some components...

// Second call: only new CSS
const additionalCSS = config.getNewCSS()
Useful for streaming SSR or code-splitting.

Media Queries

Set default active media queries for SSR:
tamagui.config.ts
const config = createTamagui({
  media,
  settings: {
    // Default to mobile on server
    mediaQueryDefaultActive: {
      xs: true,
      sm: true,
      md: true,
      lg: false,
      xl: false,
    },
  },
})
This prevents layout shifts on hydration.

Theme on Server

Default Theme

Set via TamaguiProvider:
<TamaguiProvider config={config} defaultTheme="light">
  {children}
</TamaguiProvider>

System Color Scheme

Respect user’s system preference:
tamagui.config.ts
const config = createTamagui({
  themes,
  settings: {
    shouldAddPrefersColorThemes: true,
  },
})
Generates CSS with @media (prefers-color-scheme: dark).

Theme Class on Root

Apply theme class to <html> instead of wrapper div:
tamagui.config.ts
const config = createTamagui({
  themes,
  settings: {
    themeClassNameOnRoot: true,
  },
})
<html className="t_light">
  <body>
    <TamaguiProvider config={config} defaultTheme="light">
      {children}
    </TamaguiProvider>
  </body>
</html>

Preventing Flash of Unstyled Content

Include CSS in Head

Always include CSS before body:
<html>
  <head>
    <style dangerouslySetInnerHTML={{ __html: config.getCSS() }} />
  </head>
  <body>
    {/* ... */}
  </body>
</html>

Theme Script

For theme persistence, inject a script before render:
<html>
  <head>
    <script
      dangerouslySetInnerHTML={{
        __html: `
          (function() {
            var theme = localStorage.getItem('theme') || 'light';
            document.documentElement.classList.add('t_' + theme);
          })()
        `,
      }}
    />
  </head>
  <body>
    {/* ... */}
  </body>
</html>

Compiler and SSR

The Tamagui compiler optimizes SSR:

Static Extraction

Static styles are extracted at build time:
// This is extracted to CSS at build time
const Card = styled(View, {
  backgroundColor: '$background',
  padding: '$4',
  borderRadius: '$4',
})

// SSR only outputs class names, not inline styles
<Card />  // <div class="_bg-background _p-4 _br-4" />

Tree Shaking

Unused variants are removed:
const Button = styled(View, {
  variants: {
    size: {
      small: { padding: '$2' },
      large: { padding: '$4' },  // Never used
    },
  },
})

// Only 'small' variant CSS is included
<Button size="small" />

Static Site Generation

Next.js Static Export

next.config.js
module.exports = {
  output: 'export',
}
Tamagui works seamlessly with next export.

Pre-rendering CSS

For fully static sites, pre-generate all CSS:
scripts/generate-css.ts
import config from './tamagui.config'
import fs from 'fs'

// Generate all CSS
const css = config.getCSS()

// Write to file
fs.writeFileSync('dist/tamagui.css', css)
Then include the static CSS file:
<link rel="stylesheet" href="/tamagui.css" />

Performance Tips

Use the compilerThe compiler dramatically improves SSR performance by extracting static styles at build time.
Minimize dynamic stylesDynamic styles require runtime evaluation:
// ✅ Static - optimized
<View backgroundColor="$background" padding="$4" />

// ❌ Dynamic - runtime cost
<View style={{ backgroundColor: dynamicColor }} />
Extract CSS onceCall getCSS() once and reuse:
// ✅ Good
const css = config.getCSS()

<style dangerouslySetInnerHTML={{ __html: css }} />

// ❌ Avoid
<style dangerouslySetInnerHTML={{ __html: config.getCSS() }} />
<style dangerouslySetInnerHTML={{ __html: config.getCSS() }} />

Hydration

Automatic Hydration

Tamagui handles hydration automatically:
// Server renders with inline styles
<div style="background-color: var(--background)">

// Client hydrates without re-render
// Styles match exactly

Hydration Mismatches

Avoid hydration errors:
// ❌ Bad - different on server/client
function Component() {
  const [mounted, setMounted] = useState(false)
  
  useEffect(() => setMounted(true), [])
  
  return (
    <View backgroundColor={mounted ? '$blue10' : '$red10'}>
      {/* Hydration mismatch! */}
    </View>
  )
}

// ✅ Good - consistent
function Component() {
  return (
    <View backgroundColor="$blue10">
      {/* Same on server and client */}
    </View>
  )
}

Environment Detection

Tamagui detects SSR automatically:
import { isServer, isClient } from 'tamagui'

if (isServer) {
  // Server-only code
}

if (isClient) {
  // Client-only code
}

Streaming SSR

For React 18 streaming SSR:
import { renderToPipeableStream } from 'react-dom/server'
import { TamaguiProvider } from 'tamagui'
import config from './tamagui.config'

const { pipe } = renderToPipeableStream(
  <TamaguiProvider config={config} defaultTheme="light">
    <App />
  </TamaguiProvider>,
  {
    onShellReady() {
      // Include initial CSS
      const css = config.getCSS()
      res.write(`<style>${css}</style>`)
      pipe(res)
    },
  }
)

Best Practices

Always include CSS in headEnsure CSS loads before content:
<html>
  <head>
    <style dangerouslySetInnerHTML={{ __html: config.getCSS() }} />
  </head>
  <body>{children}</body>
</html>
Set media query defaultsPrevent layout shifts:
settings: {
  mediaQueryDefaultActive: {
    xs: true,
    lg: false,
  },
}
Avoid client-only logic in renderCode that differs between server and client causes hydration errors:
// ❌ Bad
<View backgroundColor={typeof window !== 'undefined' ? '$blue' : '$red'}>

// ✅ Good - use useEffect
function Component() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])
  
  if (!mounted) return <View backgroundColor="$blue" />
  return <View backgroundColor="$red" />
}

Troubleshooting

Styles Not Appearing

Ensure CSS is included:
// Check if CSS is being generated
console.log(config.getCSS().length) // Should be > 0

Hydration Errors

Check for server/client differences:
// Enable React strict mode
<React.StrictMode>
  <App />
</React.StrictMode>

Theme Flickering

Add theme script before body:
<script dangerouslySetInnerHTML={{ __html: themeScript }} />

Build docs developers (and LLMs) love