NebulaKit renders on the server and hydrates without mismatches. The server HTML carries the full layout at the base breakpoint, so the page shows before its scripts load.
The server has no viewport, so a responsive prop renders its base value there. A plain value renders as it is.
Hydration renders the same base values, so it matches the server HTML.
Right after hydration, the values for the current breakpoint apply in one update, before the browser paints it.
On a screen above base, values set from sm upward apply at hydration, so the layout changes once.
With pt={{ base: '24px', lg: '48px' }}, a desktop shows 24px until hydration and 48px after it.
On a base screen, such as a phone, nothing changes at hydration.
When the change at hydration shows, cover the first paint with a loader until hydration.
Render it over the content, not in its place, so the server HTML still carries the page for search engines and link previews.
A cover hides the change from people, layout shift metrics still count it, so a small difference keeps them low too.
import { useEffect, useState } from 'react'import { Box, Loader, NebkitProvider } from '@nebula-kit/core' const InitialLoader = () => { const [hydrated, setHydrated] = useState(false) useEffect(() => { setHydrated(true) }, []) if (hydrated) return null return ( <Box drawable bgMode="solid" tone="neutral" position="fixed" inset="0" zIndex={9999}> <Loader centered /> </Box> )} export const Root = () => ( <NebkitProvider> <App /> <InitialLoader /> </NebkitProvider>)The server can't read localStorage, so a theme kept only there renders the default first and a user on the dark theme sees the page flash light on every load.
Save the theme a user picks in a cookie instead. It comes with every request, so the server renders the right theme from the first paint and hydration matches it.
The same goes for any other NebkitProvider setting a user picks, such as the radius.
A system theme needs no cookie. The stylesheet reads the OS setting, so the server HTML shows the right theme from the first paint. A cookie is only needed for a theme the user picks.
Read the cookie on the server, app/layout.tsx in Next.js
import { cookies } from 'next/headers'import { NebkitProvider } from '@nebula-kit/core' export default async function RootLayout({ children }: { children: React.ReactNode }) { const theme = (await cookies()).get('theme')?.value === 'dark' ? 'dark' : 'light' return ( <html> <body> <NebkitProvider theme={theme}>{children}</NebkitProvider> </body> </html> )}Write the cookie when the user switches, the server renders again with it
'use client' import { useRouter } from 'next/navigation'import { Button, type NebTheme } from '@nebula-kit/core' export const ThemeSwitch = ({ theme }: { theme: NebTheme }) => { const router = useRouter() const nextTheme = theme === 'dark' ? 'light' : 'dark' const switchTheme = () => { document.cookie = `theme=${nextTheme}; path=/; max-age=31536000; samesite=lax` router.refresh() } return <Button onClick={switchTheme}>Switch to {nextTheme}</Button>}Keep the responsive differences of the first screen small, so the change at hydration stays subtle.
Set the locale of NebkitProvider, so dates match between the server and the browser.