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:
- Server: Components render to HTML with inline styles
- CSS Extraction: Generated CSS is collected via
getCSS()
- HTML Output: CSS is included in
<head> as <style> tags
- Client Hydration: React hydrates without style flickering
- Runtime: Only dynamic styles are calculated client-side
Next.js Setup
App Router (Next.js 13+)
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>
)
}
import config from '../tamagui.config'
export default function Head() {
return (
<>
<style
dangerouslySetInnerHTML={{
__html: config.getCSS(),
}}
/>
</>
)
}
Pages Router (Next.js 12)
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>
)
}
}
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
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 }
}
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>
)
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:
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:
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:
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 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
module.exports = {
output: 'export',
}
Tamagui works seamlessly with next export.
Pre-rendering CSS
For fully static sites, pre-generate all CSS:
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 }} />
Related