Create

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.

bash
npx create-next-app@latest my-app

Install 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.

pnpmnpmyarnbun
pnpm add @wds/react @wds/core @wds/tokens lit
npm install @wds/react @wds/core @wds/tokens lit
yarn add @wds/react @wds/core @wds/tokens lit
bun add @wds/react @wds/core @wds/tokens lit

Import the tokens#

Import them once, in the root layout.

layout.tsx
// 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.

globals.css
/* 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.

page.tsx
// 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.

plan-picker.tsx
// 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.

page.tsx
// 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>
  );
}
Footer