You can customize the initial viewport of the page with the static viewport object or the dynamic generateViewport function.
Good to know: - Theviewportobject andgenerateViewportfunction exports are only supported in Server Components. - You cannot export both theviewportobject andgenerateViewportfunction from the same route segment. - If you're coming from migratingmetadataexports, you can use metadata-to-viewport-export codemod to update your changes.
viewport objectTo define the viewport options, export a viewport object from a layout.jsx or page.jsx file.
filename="layout.tsx | page.tsx" switcher
export const viewport: Viewport = {
themeColor: 'black',
}
export default function Page() {}filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: 'black',
}
export default function Page() {}generateViewport functiongenerateViewport should return a Viewport object containing one or more viewport fields.
filename="layout.tsx | page.tsx" switcher
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}In TypeScript, the params argument can be typed via PageProps<'/route'> or LayoutProps<'/route'> depending on where generateViewport is defined.
filename="layout.js | page.js" switcher
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}Good to know: - If the viewport doesn't depend on request information, it should be defined using the staticviewportobject rather thangenerateViewport.
themeColorLearn more about theme-color.
Simple theme color
filename="layout.tsx | page.tsx" switcher
export const viewport: Viewport = {
themeColor: 'black',
}filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: 'black',
}filename="<head> output" hideLineNumbers
<meta name="theme-color" content="black" />With media attribute
filename="layout.tsx | page.tsx" switcher
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}filename="<head> output" hideLineNumbers
<meta name="theme-color" media="(prefers-color-scheme: light)" content="cyan" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />width, initialScale, maximumScale and userScalableGood to know: The viewport meta tag is automatically set, and manual configuration is usually unnecessary as the default is sufficient. However, the information is provided for completeness.filename="layout.tsx | page.tsx" switcher
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
// Also supported but less commonly used
// interactiveWidget: 'resizes-visual',
}filename="layout.jsx | page.jsx" switcher
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
// Also supported but less commonly used
// interactiveWidget: 'resizes-visual',
}filename="<head> output" hideLineNumbers
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>colorSchemeLearn more about color-scheme.
filename="layout.tsx | page.tsx" switcher
export const viewport: Viewport = {
colorScheme: 'dark',
}filename="layout.jsx | page.jsx" switcher
export const viewport = {
colorScheme: 'dark',
}filename="<head> output" hideLineNumbers
<meta name="color-scheme" content="dark" />When Cache Components is enabled, generateViewport follows the same rules as other components. If viewport accesses runtime data (cookies(), headers(), params, searchParams) or performs uncached data fetching, it defers to request time.
Unlike metadata, viewport cannot be streamed because it affects initial page load UI. If generateViewport defers to request time, the page would need to block until resolved.
If viewport depends on external data but not runtime data, use use cache:
filename="app/layout.tsx" highlight={2}
export async function generateViewport() {
'use cache'
const { width, initialScale } = await db.query('viewport-size')
return { width, initialScale }
}If viewport genuinely requires runtime data, wrap the document <body> in a Suspense boundary to signal that the entire route should be dynamic:
filename="app/layout.tsx" highlight={1,13,17}
export async function generateViewport() {
const cookieJar = await cookies()
return {
themeColor: cookieJar.get('theme-color')?.value,
}
}
export default function RootLayout({ children }) {
return (
<Suspense>
<html>
<body>{children}</body>
</html>
</Suspense>
)
}Caching is preferred because it allows static shell generation. Wrapping the document body in Suspense means there is no static shell or content to immediately send when a request arrives, making the entire route block until ready on every request.
Good to know: Use multiple root layouts to isolate fully dynamic viewport to specific routes, while still letting other routes in your application generate a static shell.
You can add type safety to your viewport object by using the Viewport type. If you are using the built-in TypeScript plugin in your IDE, you do not need to manually add the type, but you can still explicitly add it if you want.
viewport object
export const viewport: Viewport = {
themeColor: 'black',
}generateViewport function
export function generateViewport(): Viewport {
return {
themeColor: 'black',
}
}
type Props = {
params: Promise<{ id: string }>
searchParams: Promise<{ | string[] | undefined }>
}
export function generateViewport({ params, searchParams }: Props): Viewport {
return {
themeColor: 'black',
}
}
export default function Page({ params, searchParams }: Props) {}For JavaScript projects, you can use JSDoc to add type safety.
/** @type {import("next").Viewport} */
export const viewport = {
themeColor: 'black',
}| Version | Changes |
|---|---|
v14.0.0 | viewport and generateViewport introduced. |