Create

Frameworks

React

Install and use WDS in a React app.

@wds/react has a typed React component for every wds-* element. A component sets properties on its element and passes the element's events to on<Event> props.

Installation#

Create project#

Skip this step if you already have a React app. The example uses Vite.

bash
npm create vite@latest my-app -- --template react-ts

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 entry file.

main.tsx
// src/main.tsx
import '@wds/tokens/tokens.css';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(<App />);

Use components#

Import a component from its entry point, such as @wds/react/button. The import also registers the element. An event such as wds-change is the onWdsChange prop.

App.tsx
import { useState } from 'react';
// Every component has its own entry point, so the bundle gets only what you use.
import { WdsButton } from '@wds/react/button';
import { WdsCard } from '@wds/react/card';
import { WdsFieldError } from '@wds/react/field';
import { WdsRadio, WdsRadioGroup } from '@wds/react/radio';

export function App() {
  const [plan, setPlan] = useState('free');
  const [busy, setBusy] = useState(false);

  return (
    <WdsCard heading="Plan">
      {/* 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>

      {/* booleans, arrays and objects reach the element as properties */}
      <WdsFieldError errors={[{ message: 'Choose a plan' }]} />

      <WdsButton slot="footer" loading={busy} onClick={() => setBusy(true)}>
        Save
      </WdsButton>
    </WdsCard>
  );
}

Forms#

Form controls are form-associated, so they send their values with the form like native fields.

Signup.tsx
import { WdsButton } from '@wds/react/button';
import { WdsCheckbox } from '@wds/react/checkbox';
import { WdsInput } from '@wds/react/input';

export function Signup() {
  async function action(data: FormData) {
    // wds-* controls are in FormData like native fields
    await fetch('/api/signup', { method: 'POST', body: data });
  }

  return (
    <form action={action}>
      <WdsInput name="email" type="email" required />
      <WdsCheckbox name="newsletter">Newsletter</WdsCheckbox>
      <WdsButton type="submit">Sign up</WdsButton>
    </form>
  );
}
Footer