afaqrashid.com  ·  portfolio · 2026 ed.

Next.js "Unsupported metadata viewport" Warning: Tested Fix

Next.js prints "Unsupported metadata viewport is configured in metadata export" when viewport, themeColor or colorScheme sit inside your metadata object. Those fields moved to a separate viewport export. The build still passes, which is why the warning is easy to ignore, but the tags it warns about never reach the page.

I tested this on Next.js 16.4.4 with next build, next start and next dev, then read the meta tags each page sent.

What the old setup outputs

This page puts all three fields in metadata, the way older guides show:

// app/a/page.tsx
export const metadata = {
  title: "A",
  viewport: { width: "device-width", initialScale: 1, maximumScale: 1 },
  themeColor: "#ff0000",
  colorScheme: "dark",
};

next build exited with code 0 and printed three warnings, one per field. next dev printed them again on every request. The page shipped with only this:

<meta name="viewport" content="width=device-width, initial-scale=1"/>

That is Next.js's default viewport tag, which it adds to every page. My maximumScale was gone, and there was no theme-color or color-scheme tag at all.

The fix: a viewport export

// app/b/page.tsx
import type { Metadata, Viewport } from "next";

export const metadata: Metadata = { title: "B" };

export const viewport: Viewport = {
  themeColor: "#ff0000",
  colorScheme: "dark",
};

With the same values moved over, all three tags came out:

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<meta name="theme-color" content="#ff0000"/>
<meta name="color-scheme" content="dark"/>

You don't need to set width or initialScale yourself. Next.js adds width=device-width, initial-scale=1 to every page either way.

Leave maximumScale out

My test copied maximumScale: 1 from an old guide. I ran Lighthouse's accessibility check on a page with it, and its meta-viewport audit failed: it flags a maximum-scale below 5 because it stops people zooming in. The same audit passed on a page without it.

How layout and page viewports combine

I put a light and dark theme color in the root layout:

// app/layout.tsx
export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "#ffffff" },
    { media: "(prefers-color-scheme: dark)", color: "#000000" },
  ],
};

Every page got one theme-color tag per color scheme, each with its media attribute. A page that exported only colorScheme: "dark" kept both theme colors from the layout and added its color-scheme tag. So unlike metadata.openGraph, the viewport fields combine one by one.

A page that set its own themeColor replaced the layout's pair with its single color.

Theme color from data: generateViewport

If the color comes from a CMS or database, use generateViewport, the viewport version of generateMetadata:

export async function generateViewport(): Promise<Viewport> {
  const brand = await getBrand(); // your own lookup
  return { themeColor: brand.color };
}

In my test it returned a fixed #00ff00 and the page shipped <meta name="theme-color" content="#00ff00"/>. A file can export viewport or generateViewport, but not both.

Move them with the codemod

Next.js ships a codemod for this. I ran it on a copy of the old page:

npx @next/codemod@latest metadata-to-viewport-export app

It reported 1 file changed and moved all three fields into a new export:

export const metadata = {
  title: "A"
};

export const viewport = {
  width: "device-width",
  colorScheme: "dark",
  themeColor: "#ff0000"
};

Review the result before you commit. It keeps whatever values you had, maximumScale included, and doesn't add the Viewport type.

Find every old field at once

grep -rnE "^\s*(viewport|themeColor|colorScheme):" app --include="*.ts" --include="*.tsx"

Any hit inside a metadata object or generateMetadata return value needs to move to a viewport export. Then check the page source for the theme-color tag, or paste the HTML into the metadata checker below.

Related

Sources