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.

npm i tuix-webcomponents @lit/react

Add the design tokens once, at the root of the app. In Next.js that is app/layout.tsx:

app/layout.tsx
import 'tuix-webcomponents/dist/assets/tuix-styles.css'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en">
            <head>
                <link rel="preconnect" href="https://hccdn.tuix.ch" />
            </head>
            <body>{children}</body>
        </html>
    )
}

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):

src/components/InvoiceForm.tsx
'use client'
 
import { useState } from 'react'
import { TuixButton, TuixDropdown, TuixInput } from 'tuix-webcomponents/react'
import { DescriptionType, IconName, MainVariant } from 'tuix-webcomponents'
 
const COUNTRIES = [
    { name: 'Switzerland', value: 'CH' },
    { name: 'Germany', value: 'DE' },
]
 
export default function InvoiceForm() {
    const [name, setName] = useState('')
    const [country, setCountry] = useState<string>()
    const [error, setError] = useState<string>()
 
    const save = () => {
        if (!name) return setError('A company name is required')
        console.log(name, country)
    }
 
    return (
        <>
            <TuixInput
                title="Company name"
                value={name}
                iconRight={IconName.BUILDING}
                description={error}
                descriptionType={error ? DescriptionType.ERROR : DescriptionType.TEXT}
                onChange={(event) => {
                    setName(event.detail.value)
                    setError(undefined)
                }}
            />
 
            <TuixDropdown
                title="Country"
                options={COUNTRIES}
                value={COUNTRIES.find((option) => option.value === country)}
                onSelect={(event) => setCountry(String(event.detail.value))}
            />
 
            <TuixButton variant={MainVariant.PRIMARY} iconLeft={IconName.SAVE} onCustomClick={save}>
                Save
            </TuixButton>
        </>
    )
}

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.

ElementPropEventevent.detail
tuix-button, tuix-icon-buttononCustomClickcustomClickNone
tuix-inputonChange, onBluron-change, on-blur{ value: string }
tuix-inputonClickon-clickNone
tuix-dropdownonSelectselect{ value: string | number | boolean }
tuix-dropdownonInputSingleinput-single{ value: string }
tuix-checkbox, tuix-toggle-buttononChangechange{ value: boolean, label?: string }
tuix-radio-buttononChangechange{ value: string }
tuix-tableonNextPage, onPreviousPage, onPageSizeChangenext-page, previous-page, page-size-change{ page: number, pageSize: number }
tuix-toast, tuix-modalonCloseon-closeNone

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:

const THEME_INIT = `document.documentElement.setAttribute('data-theme',
    localStorage.getItem('theme') === 'dark' ? 'dark' : 'light')`
 
<script dangerouslySetInnerHTML={{ __html: THEME_INIT }} />

Static export (output: 'export') works. If a page imports the wrappers at module scope and something in them touches window, load them lazily instead:

const TuixTable = dynamic(() => import('tuix-webcomponents/react').then((m) => m.TuixTable), {
    ssr: false,
})

Without the wrappers

For a one-off, render the tag directly and declare it to TypeScript:

src/types/tuix.d.ts
import type React from 'react'
 
declare global {
    namespace JSX {
        interface IntrinsicElements {
            'tuix-text': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
                size?: string
                color?: string
                dark?: boolean
            }
        }
    }
}
import 'tuix-webcomponents' // registers the elements
 
;<tuix-text size="xl" color="primary">
    Hello
</tuix-text>

Remember that this path sets attributes: lowercase names only, and no objects, arrays or event handlers.

Troubleshooting

SymptomCause
ReferenceError: HTMLElement is not definedThe 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 assignableThe prop is typed with an enum. Pass MainVariant.PRIMARY rather than the string
The field never changesThe value prop is not written back in the change handler
onCustomClick never firesThe 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 fontThe design tokens stylesheet is not imported, and it is what pulls Rubik in
Nothing renders at allNeither the wrappers nor the library were imported, so the tag was never registered

On this page