Use the server-safe Card, create client islands, and avoid hydration issues in Next.js and other React 19 frameworks.
The package targets React 19 and works in server-first frameworks. Most
components are client components because they manage interaction, focus, or
portals. Card is the exception: it is a plain layout primitive with no state,
so it can render entirely on the server.
| Component | Boundary |
|---|---|
Card | Server-safe |
Button, Input, Checkbox, CheckboxGroup, Switch, Slider, RadioGroup, Select, Progress | Client |
Field, Fieldset, Form | Client |
Tabs, Accordion, Toggle, ToggleGroup | Client |
Dialog, AlertDialog, Drawer, Popover, Menu, Tooltip, Toast | Client |
ScrollArea | Client |
A Server Component can import and render a Client Component directly. The client component is serialized into the HTML during server rendering and hydrates in the browser. What cannot cross the boundary is a function defined in a Server Component and passed as a prop.
Use a deep import when you want to guarantee that only the server-safe module is loaded:
import { Card } from "@xingxingmofashu/ui/components/card";
export default function Page() {
return (
<Card
title="Server rendered"
description="This card is part of the initial HTML."
>
<p className="text-sm text-ui-ink/70">
No client JavaScript is required for the card itself.
</p>
</Card>
);
}The root import works too, but the deep import makes the boundary explicit.
Move state, effects, and event handlers into a client component:
"use client";
import { Settings } from "lucide-react";
import { useState } from "react";
import { Button, Dialog, Switch } from "@xingxingmofashu/ui";
export function SettingsDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button
label="Open settings"
leadingIcon={<Settings />}
onClick={() => setOpen(true)}
/>
<Dialog
open={open}
onOpenChange={setOpen}
title="Workspace settings"
description="Choose how your workspace behaves."
footer={<Button label="Done" onClick={() => setOpen(false)} />}
>
<Switch label="Email notifications" defaultChecked />
</Dialog>
</>
);
}Then render the island from a Server Component:
import { SettingsDialog } from "./settings-dialog";
export default function Page() {
return (
<main>
<h1>Workspace</h1>
<SettingsDialog />
</main>
);
}Split at the smallest interactive boundary. A page with one button does not need to become a client component; a settings form probably does.
The first HTML and the first client render must agree. Two rules prevent most hydration warnings:
window, document, or localStorage during render. Read them
in an effect or set the value before hydration.data-theme attribute before the first paint.A common pattern is a blocking script in the document head that applies the
stored theme to <html>, plus suppressHydrationWarning because the class may
differ between the server and the client:
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
);
}A theme library such as next-themes can manage the script and storage for you.
Apply the same idea in any framework: resolve the theme before React hydrates,
not inside a component body.
Portals are also client-only in practice. Dialog, Drawer, and Popover
render their popup into a portal when they open, so the trigger and the open
state must be available on the client.
The documentation site is a static export, and the same approach works for any content site:
const nextConfig = {
output: "export",
images: { unoptimized: true },
};
export default nextConfig;With output: "export", next build writes HTML files instead of starting a
server. The interactive components still hydrate normally, and Card is
already present in the exported HTML. If you host under a subpath, configure
basePath and make asset URLs respect it.
The Vite starter uses the same package without a server boundary:
pnpm dlx degit xingxingmofashu/ui/templates/starter my-app
cd my-app
pnpm install
pnpm devImport the package stylesheet once in src/index.css, then render components
from src/App.tsx. See Templates for the full
walkthrough.
@xingxingmofashu/ui CSS from a component instead of the global
stylesheet.onClick from a Server Component to a client component.localStorage or matchMedia while rendering."use client" on a wrapper that uses state.Card content appears in the generated
HTML.