SSR safety
Kalyx is designed for server rendering. Every picker has a renderToString test in the component test suite, and the published entries carry the 'use client' directive for the Next.js App Router and other RSC setups.
What Kalyx does
- Stable IDs via React's
useId(), so they match between server and client. - No
window/documentat module scope. All DOM access is insideuseEffect. - No
useLayoutEffectin hot paths, which avoids the SSR warning. - Floating UI handles positioning with its SSR-safe
useFloatinghook.
In short: importing @kalyx/react on a server is safe.
Usage with Next.js App Router
Kalyx components use React Context and interactivity, so they must render in a client component.
// app/booking/date-field.tsx
'use client';
import { useState } from 'react';
import { DatePicker, type ISODateString } from '@kalyx/react';
export function DateField() {
const [date, setDate] = useState<ISODateString | null>(null);
return (
<DatePicker value={date} onChange={setDate}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
);
}
Then consume it from a server component:
// app/booking/page.tsx
import { DateField } from './date-field';
export default function Page() {
return (
<main>
<h1>Book a date</h1>
<DateField />
</main>
);
}
RSC (React Server Components)
Kalyx components are client components, and the published bundle already carries the
'use client' directive. packages/react/tsup.config.ts prepends it to every built
entry (dist/index.js, dist/index.cjs, dist/headless.js, dist/headless.cjs). You
can import a picker directly inside a Server Component file's module graph without
writing a boundary of your own.
A wrapper module is therefore optional. It is still worth writing when you want to co-locate app-specific defaults:
// components/ui/date-field.tsx
'use client';
export { DatePicker } from '@kalyx/react';
Uncontrolled rendering on the server
In SSR, the initial render has no user interaction yet. Start with either defaultValue or value={null}:
{/* Uncontrolled: good for forms */}
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input name="checkIn" />
</DatePicker>
{/* Controlled with null: good for optional fields */}
<DatePicker value={null} onChange={setDate}>
<DatePicker.Input />
</DatePicker>
Hydration caveats
If your displayFormat or locale differ between server and client (for example, reading navigator.language at render time), you'll see hydration mismatches. Determine the locale before rendering:
// Read locale from cookies / session server-side
<DatePicker locale={cookieLocale} value={iso} onChange={setIso}>
...
</DatePicker>
Avoid reading navigator, window.matchMedia, or Intl.DateTimeFormat().resolvedOptions() in the render body. Read them inside useEffect and defer rendering if needed.
Testing SSR
Add a smoke test that mirrors production rendering:
import { renderToString } from 'react-dom/server';
import { DatePicker } from '@kalyx/react';
it('renders on the server without throwing', () => {
const html = renderToString(
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>,
);
expect(html).toContain('input');
});