React and Next.js
Using the components in a React or Next.js application through the generated wrappers.
The package ships a React wrapper for every element under
tuix-webcomponents/react, generated with
@lit/react. The wrappers take each
element's own properties as props, so objects and arrays pass through untouched,
and they turn every custom event into an on* prop. Nothing needs wrapping by
hand.
React 18 renders a custom element but passes every prop as a string attribute and cannot listen to custom events,
which is what the wrappers solve. React 19 handles both natively; the wrappers are still worth using there for the
typed props and the on* event props.
1. Install
@lit/react and react are peer dependencies of the wrappers.
Add the design tokens once, at the root of the app. In Next.js that is
app/layout.tsx:
That stylesheet also pulls Rubik in from the Tuix CDN, so the typography is covered. See Theming for requesting the font earlier with its own link.
2. Use them
Importing from tuix-webcomponents/react registers the custom elements, so
there is nothing else to set up. Props carry the property names
(iconRight, not iconright):
The components are uncontrolled from React's point of view: they report a change and wait for the new prop. Always write the value back in the handler, as above, or the field will appear frozen.
The props are typed from the element's own properties, and those properties are typed with the exported enums. A
string literal is therefore not assignable: variant={MainVariant.PRIMARY} compiles, variant="primary" does not.
The enums are listed in Enums and types.
Event props
Each custom event becomes on plus its name in PascalCase, with an event whose
name already begins with on keeping a single prefix. The handler receives the
CustomEvent, typed with its payload.
| Element | Prop | Event | event.detail |
|---|---|---|---|
tuix-button, tuix-icon-button | onCustomClick | customClick | None |
tuix-input | onChange, onBlur | on-change, on-blur | { value: string } |
tuix-input | onClick | on-click | None |
tuix-dropdown | onSelect | select | { value: string | number | boolean } |
tuix-dropdown | onInputSingle | input-single | { value: string } |
tuix-checkbox, tuix-toggle-button | onChange | change | { value: boolean, label?: string } |
tuix-radio-button | onChange | change | { value: string } |
tuix-table | onNextPage, onPreviousPage, onPageSizeChange | next-page, previous-page, page-size-change | { page: number, pageSize: number } |
tuix-toast, tuix-modal | onClose | on-close | None |
Nine of these props share a name with a React DOM prop: onChange, onBlur and onClick on TuixInput,
onSelect on TuixDropdown, onChange on the checkbox, the radio group and the toggle, and onClose on the toast
and the modal. On a wrapper the prop always means the custom event in the table above, never React's own. onClick
on a TuixInput is the element's on-click, not a React MouseEvent. When a native handler is what you need, put
it on a wrapping element.
The wrapper for each element is named after the element itself: TuixButton,
TuixIcon, TuixText, TuixPaper, TuixLink, TuixIconButton, TuixInput,
TuixDropdown, TuixCheckbox, TuixRadioButton, TuixToggleButton,
TuixTable, TuixToast, TuixModal.
3. Next.js specifics
Mark the pages as client code. Custom elements need a DOM. Put
'use client' at the top of any component that renders one.
Server rendering is fine. The server prints <tuix-button> with its
children; the element upgrades on the client and moves the children into
its slot. Nothing is measured or generated randomly, so hydration
matches.
Set the theme before first paint if the app has a dark mode, so the
components' dark prop and the page agree from the first frame:
Static export (output: 'export') works. If a page imports the
wrappers at module scope and something in them touches window, load
them lazily instead:
Without the wrappers
For a one-off, render the tag directly and declare it to TypeScript:
Remember that this path sets attributes: lowercase names only, and no objects, arrays or event handlers.
Troubleshooting
| Symptom | Cause |
|---|---|
ReferenceError: HTMLElement is not defined | The library was imported on the server. Move the import into a client component, or dynamic(..., { ssr: false }) |
Cannot find module '@lit/react' | @lit/react is missing; it is a peer dependency, installed alongside the package |
Cannot find module 'tuix-webcomponents/react' | The resolver ignores the package's exports field; TypeScript needs moduleResolution: "bundler" |
Type '"primary"' is not assignable | The prop is typed with an enum. Pass MainVariant.PRIMARY rather than the string |
| The field never changes | The value prop is not written back in the change handler |
onCustomClick never fires | The tag is used directly instead of the wrapper, so React attached no listener |
An array or object prop arrives as [object Object] | The tag is used directly instead of the wrapper, so React set it as an attribute |
| Text renders in the wrong font | The design tokens stylesheet is not imported, and it is what pulls Rubik in |
| Nothing renders at all | Neither the wrappers nor the library were imported, so the tag was never registered |