Next.js (App Router)
@ship-it-ui/next is an optional companion package for App Router apps. It
ships three surfaces:
<ThemeBootstrap />— synchronous inline script that reads theship-it-themecookie and sets<html data-theme>before first paint, killing the dark→light flash for users on the light theme.getThemeFromCookies(await cookies())— server helper for the App Routercookies()store. Use the return value to seeddata-themeon<html>for the first server-rendered frame.<ThemeToggle />— a token-styledSwitchbound to the active theme. ReusesuseThemefrom@ship-it-ui/uifor client state and writes the cookie so the next request renders the correct theme synchronously.
Baseline: Next.js 16 / React 19.2. The package's peer range is
next ^15.0.0 || ^16.0.0 and react ^19.0.0.
Install#
pnpm add @ship-it-ui/nextWire it up#
// app/layout.tsx
import { cookies } from 'next/headers';
import { ThemeBootstrap } from '@ship-it-ui/next';
import { getThemeFromCookies } from '@ship-it-ui/next/server';
import '@ship-it-ui/ui/styles/globals.css';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const theme = getThemeFromCookies(await cookies());
return (
<html lang="en" data-theme={theme}>
<head>
<ThemeBootstrap />
</head>
<body>{children}</body>
</html>
);
}Import
getThemeFromCookiesfrom@ship-it-ui/next/server— the root entrypoint includes client components (<ThemeToggle />) and is tagged'use client', so calling its server helpers from a server layout would turn them into client-reference proxies. The/serversubpath ships just the cookie helpers and is safe to import anywhere.
The bootstrap script runs synchronously, so it executes before the body
hydrates — no flash. The server-rendered data-theme attribute lines up with
what the script will set, so the cookie + SSR paths agree.
Drop in the toggle#
import { ThemeToggle } from '@ship-it-ui/next';
<ThemeToggle label="Light theme" />;The toggle writes a year-long cookie (SameSite=Lax, non-sensitive). The
onThemeChange callback fires after persistence, which is handy for
analytics.
Plain React fallback#
next is an optional peer dependency — the package works in non-Next React
apps that adopt the same cookie convention. Just skip getThemeFromCookies
and call readThemeCookie() from a top-level effect instead.
Cache Components opt-in (runtime consumers)#
If your app deploys to a Next.js runtime (Vercel, Cloudflare, Node, etc.) instead of static export, you can wrap the cookie lookup in a Cache Components boundary so theme reads are deduplicated and invalidated explicitly:
// app/layout.tsx
import { cacheLife, cacheTag } from 'next/cache';
import { cookies } from 'next/headers';
import { ThemeBootstrap } from '@ship-it-ui/next';
import { getThemeFromCookies } from '@ship-it-ui/next/server';
async function readTheme() {
'use cache';
cacheLife('hours');
cacheTag('theme');
return getThemeFromCookies(await cookies());
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const theme = await readTheme();
return (
<html lang="en" data-theme={theme}>
<head>
<ThemeBootstrap />
</head>
<body>{children}</body>
</html>
);
}cacheTag('theme') lets you invalidate cached theme reads from a server
action (revalidateTag('theme')) when a user updates their preference —
the same cookie write the <ThemeToggle /> already performs.
The docs site you are reading uses output: 'export' (static export to
GitHub Pages), so Cache Components is a no-op for it. The recipe is here
for apps that opt in to the runtime.