Frameworks
Next.js
Install and use WDS in a Next.js app.
@wds/react has a typed React component for every wds-* element. The components are Client Components, and you can render them from Server Components. The guide uses the App Router.
Installation#
Create project#
Skip this step if you already have a Next.js app.
npx create-next-app@latest my-appInstall packages#
@wds/core and lit are peer dependencies, so the app has one copy of each. A second @wds/core would define the same wds-* elements again.
pnpm add @wds/react @wds/core @wds/tokens litnpm install @wds/react @wds/core @wds/tokens lityarn add @wds/react @wds/core @wds/tokens litbun add @wds/react @wds/core @wds/tokens litImport the tokens#
Import them once, in the root layout.
// app/layout.tsx
import '@wds/tokens/tokens.css';
import './globals.css';
export default function RootLayout({ children }: LayoutProps<'/'>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Hide elements until they load#
The server sends the elements as empty tags: they get their properties and styles in the browser, after hydration. This rule hides them until they are registered.
/* app/globals.css */
/* Hides the elements until they are registered. */
:not(:defined) {
visibility: hidden;
}Usage#
Server Components#
A Server Component can render the components and pass them strings, numbers and booleans. Import each one from its entry point, such as @wds/react/button: the import also registers the element.
// app/page.tsx: a Server Component
import { WdsBadge } from '@wds/react/badge';
import { WdsButton } from '@wds/react/button';
import { WdsCard } from '@wds/react/card';
export default function Home() {
return (
<WdsCard heading="Plan" description="You are on the free plan.">
<WdsBadge>Free</WdsBadge>
<WdsButton slot="footer" href="/upgrade">Upgrade</WdsButton>
</WdsCard>
);
}Client Components#
Event handlers and state need a Client Component, like any interactive code in Next.js. An event such as wds-change is the onWdsChange prop.
// app/plan-picker.tsx
'use client';
import { useState } from 'react';
import { WdsRadio, WdsRadioGroup } from '@wds/react/radio';
export function PlanPicker() {
const [plan, setPlan] = useState('free');
return (
// the wds-change event is the onWdsChange prop
<WdsRadioGroup value={plan} onWdsChange={(e) => setPlan(e.detail.value)}>
<WdsRadio value="free">Free</WdsRadio>
<WdsRadio value="pro">Pro</WdsRadio>
</WdsRadioGroup>
);
}Forms#
Form controls are form-associated, so a Server Action gets their values in FormData like native fields.
// app/signup/page.tsx
import { WdsButton } from '@wds/react/button';
import { WdsCheckbox } from '@wds/react/checkbox';
import { WdsInput } from '@wds/react/input';
async function signup(data: FormData) {
'use server';
// wds-* controls are in FormData like native fields
await createUser({ email: data.get('email'), newsletter: data.has('newsletter') });
}
export default function Signup() {
return (
<form action={signup}>
<WdsInput name="email" type="email" required />
<WdsCheckbox name="newsletter">Newsletter</WdsCheckbox>
<WdsButton type="submit">Sign up</WdsButton>
</form>
);
}