SolidJS
mtrl/solid has a SolidJS component for every mtrl web component, for Solid 1.8 or later and
SolidStart. Each one renders its <m-*> element, so it looks and behaves exactly as the element
does. This page covers only what is particular to Solid; Getting started
has the install and the base stylesheet, and each component page has its options and events.
Components and props
The components are named after the element: Button, Switch, Textfield, Tabs. Import
them where you use them. A component's props are its element's attributes, in camelCase:
supportingText for supporting-text, ariaLabel for aria-label.
import { Button, Slider, Textfield } from 'mtrl/solid';
export function Profile() {
return (
<>
<Textfield label="Email" type="email" supportingText="We never share it" required />
<Slider ariaLabel="Volume" min={0} max={100} step={5} />
<Button variant="filled">Save</Button>
</>
);
}
A component registers its element the first time it mounts, and importing mtrl/solid loads
the elements' styles, so the base stylesheet is the only CSS you add.
Props stay reactive: pass a signal's value and the element follows it. On the server they are
rendered as attributes, so the markup carries them. In the browser, Solid sets a custom
element's props as properties; every mtrl element, and every child like <Tab>, has a property
for each attribute, so both reach the same place. false removes a boolean attribute. Anything
else you pass, such as class, id or data-*, lands on the element.
Events
Element events are on props in PascalCase: onChange, onInput, onOpen, onClose,
onSelect. The handler receives the element's CustomEvent, with the data in detail; every
component page lists its events and their fields.
import { Switch, Textfield } from 'mtrl/solid';
function search(query: string) {
console.log('Searching for', query);
}
export function Settings() {
return (
<>
<Switch onChange={(event) => console.log('Wi-Fi', event.detail.checked)}>Wi-Fi</Switch>
<Textfield label="Search" onInput={(event) => search(event.detail.value)} />
</>
);
}
The events are typed, so event.detail is checked in your editor. Native events such as
onClick or onFocus are not the component's own: Solid handles them as on any tag, with the
browser's event.
Signals, controlled and uncontrolled
Solid has no two-way binding, so a value is controlled as in React: pass the signal's value to
the live property (checked, value, selected) and set the signal from the event. The
component writes the property to the element whenever the signal changes.
import { createSignal } from 'solid-js';
import { Switch, Tabs, Tab } from 'mtrl/solid';
export function Library() {
const [wifi, setWifi] = createSignal(true);
const [tab, setTab] = createSignal<string | null>('songs');
return (
<>
<Switch checked={wifi()} onChange={(event) => setWifi(event.detail.checked)}>Wi-Fi</Switch>
<Tabs value={tab()} onChange={(event) => setTab(event.detail.value)}>
<Tab value="songs">Songs</Tab>
<Tab value="albums">Albums</Tab>
</Tabs>
<p>Wi-Fi is {wifi() ? 'on' : 'off'}, showing {tab()}.</p>
</>
);
}
A controlled prop without its event doesn't hold the element: as with a native input,
checked={false} alone lets the user turn the switch on, and the signal falls out of step.
To leave the state to the element, don't pass the live property. Its attribute is the
element's default, as checked is on a native checkbox, and takes the default prefix:
defaultChecked, defaultValue. Read the value from the event when you need it:
import { Switch } from 'mtrl/solid';
export function Notifications() {
return (
<Switch name="notifications" defaultChecked onChange={(event) => console.log('Notifications', event.detail.checked)}>
Notifications
</Switch>
);
}
A dialog, a sheet or a menu opens from its open attribute. Pass a signal, and set it back when
the user closes it:
import { createSignal } from 'solid-js';
import { Button, Dialog } from 'mtrl/solid';
export function DeleteDraft() {
const [open, setOpen] = createSignal(false);
return (
<>
<Button onClick={() => setOpen(true)}>Delete</Button>
<Dialog
open={open()}
headline="Delete draft?"
actions={
<>
<Button variant="text" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="text" onClick={() => setOpen(false)}>Delete</Button>
</>
}
onClose={() => setOpen(false)}
>
<p>The draft will be deleted for good.</p>
</Dialog>
</>
);
}
Refs
ref gives the <m-*> element itself, with its properties and methods, once it is created.
Type it with the element's type from mtrl/elements, and use it in onMount or an event
handler. A callback, ref={(element) => …}, works too.
import { Button, Textfield } from 'mtrl/solid';
import type { TextfieldElement } from 'mtrl/elements';
export function Name() {
let field: TextfieldElement | undefined;
return (
<>
<Textfield ref={field} label="Name" defaultValue="Ada Lovelace" />
<Button onClick={() => field?.select()}>Select the name</Button>
</>
);
}
Children
A component's children become the element's content: a button's label, a dialog's or a card's
text. A component's named regions, such as a dialog's headline and actions or a top app
bar's leading and trailing, are the element's named slots: pass them as props of those names
taking JSX, as the dialog above does (a dashed slot is camelCased: headerAction). headline
takes text for the attribute or JSX for the slot.
Lists of items are declared with child components: Tab, Radio, Chip, ListItem,
MenuItem, SelectOption, SearchSuggestion, NavigationRailItem, DrawerItem,
ButtonGroupItem and CarouselItem. They render nothing themselves; their parent reads them.
Keep them direct children of their parent, with no element in between. <For> and <Show> are
fine: the parent reads them again when they change.
import { createSignal, For } from 'solid-js';
import { Radios, Radio } from 'mtrl/solid';
const sizes = [{ value: 's', label: 'Small' }, { value: 'm', label: 'Medium' }, { value: 'l', label: 'Large' }];
export function Size() {
const [size, setSize] = createSignal<string | null>('m');
return (
<Radios value={size()} onChange={(event) => setSize(event.detail.value)} ariaLabel="Size">
<For each={sizes}>{(option) => <Radio value={option.value}>{option.label}</Radio>}</For>
</Radios>
);
}
Solid sets a child's props as properties, often before mtrl has defined the elements. mtrl
keeps them from 0.10.0-next.2 on; with an earlier version, a <Tab> rendered in the browser
lost its value.
Bare tags in JSX
To write the elements themselves (<m-switch checked>), with Solid's prop: and on: forms,
import the tag types once, in any file TypeScript sees:
import type {} from 'mtrl/solid/jsx';
export const Wifi = () => <m-switch checked supporting-text="Saves battery">Wi-Fi</m-switch>;
It is types only; the element still needs defineAll() or its define function.
SolidStart and server rendering
The components render on the server. SolidStart's HTML has each element's tag, its attributes
and its children, and a controlled value as its attribute: a switch whose signal is on renders
<m-switch checked>. Every module imports safely without a DOM, and the element registers in
onMount, so it upgrades as the page hydrates. SolidStart adds the hydration script Solid
needs; a hand-made server render puts generateHydrationScript() in the page's head. You don't
need clientOnly for these components, and a ref is only set in the browser.
Until an element upgrades, it has no shadow root and so none of its styles. The pre-upgrade
stylesheet gives each element its final size and look meanwhile, so the page doesn't shift when
the script arrives. Import it once in src/app.tsx:
import 'mtrl/elements/preupgrade.css';
Server rendering explains what the server sends and how the upgrade happens, for every framework.