Migrate from Angular Material
Angular Material's components are Angular directives and services; lit-material's are custom elements, which Angular templates use with the schema for them. Here is each component's counterpart, the binding and forms patterns, and what replaces the services.
Set up an Angular app
Install the tokens and the components you use, and load the tokens stylesheet once, from angular.json's styles or your global stylesheet:
npm install @lit-material/tokens @lit-material/button @lit-material/text-field
/* src/styles.css */
@import "@lit-material/tokens/css/index.css";
Import each package for its side effect (it registers the elements) and add CUSTOM_ELEMENTS_SCHEMA to every component whose template uses them:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@lit-material/button";
import "@lit-material/text-field";
@Component({
selector: "app-profile",
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<lit-material-text-field label="Name" [value]="name" (input)="name = $any($event.target).value"></lit-material-text-field>
<lit-material-button variant="filled" [disabled]="!name" (click)="save()">Save</lit-material-button>
`,
})
export class ProfileComponent {
name = "";
save() {}
}
Angular Material's Sass theme doesn't carry over. Set the --md-sys-color-* tokens, or make a palette with the theme builder.
Properties, attributes and events
[value] sets a property, which is how arrays and objects go in ([rows]="rows", [options]="options"); [attr.aria-label] sets an attribute; (change) listens to an event, hyphenated names included: (page-change)="onPage($any($event).detail)". Under CUSTOM_ELEMENTS_SCHEMA Angular doesn't type-check these bindings, and $event.target is a plain EventTarget, hence $any(). Each package's dist/web-types.json and VS Code custom data give completion in templates; see React, Vue and Angular.
Forms
lit-material ships no ControlValueAccessor, so formControlName and ngModel don't work on its elements by themselves. What the elements do, checked in Chromium on the elements themselves (not inside an Angular app):
- Text field, textarea, slider: fire
inputon the element withvaluealready updated, andblurreaches the element; settingvalueupdates the field. That is what Angular'sDefaultValueAccessoruses (it writesvalueand listens forinputandblur), songDefaultControlselects it:<lit-material-text-field label="Email" formControlName="email" ngDefaultControl>. A slider's value is a number. - Select: fires only
change, songDefaultControlwould never update the control. Bind[value]and(change), or use an accessor like the one below withvalue. - Checkbox, switch, radio: the state is
checkedand the event ischange:[checked]="on" (change)="on = $any($event.target).checked", or an accessor.
An accessor for the checked-state elements is a few lines in your app (it isn't part of lit-material):
import { Directive, ElementRef, HostListener, forwardRef, inject } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
@Directive({
selector: "lit-material-checkbox[formControlName], lit-material-checkbox[formControl], lit-material-checkbox[ngModel], lit-material-switch[formControlName], lit-material-switch[formControl], lit-material-switch[ngModel]",
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckedAccessor), multi: true }],
})
export class CheckedAccessor implements ControlValueAccessor {
private readonly el = inject<ElementRef<HTMLElement & { checked: boolean; disabled: boolean }>>(ElementRef).nativeElement;
private onChange = (_: boolean) => {};
private onTouched = () => {};
@HostListener("change") changed() { this.onChange(this.el.checked); }
@HostListener("blur") blurred() { this.onTouched(); }
writeValue(value: boolean) { this.el.checked = !!value; }
registerOnChange(fn: (value: boolean) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(disabled: boolean) { this.el.disabled = disabled; }
}
The fields validate themselves (required, minlength, pattern, min/max) and show the browser's message once touched; Angular's validators run separately. To show an Angular error on a field, bind [error] and [errorText]. Every form field takes part in a native <form>'s FormData.
Component by component
| Angular Material | lit-material | Package | Notes |
|---|---|---|---|
mat-button, mat-flat-button, mat-stroked-button, mat-raised-button (matButton) | lit-material-button | @lit-material/button | variant: text, filled, outlined, elevated, tonal. href renders a link. |
mat-icon-button | lit-material-icon-button | @lit-material/icon-button | Icon in slot="icon"; toggle + selected for a toggle button. Always give it an aria-label. |
mat-fab, mat-mini-fab, extended fab | lit-material-fab | @lit-material/fab | size="small" for mini, extended with label text; color picks the container colour. |
mat-form-field + matInput | lit-material-text-field, lit-material-textarea | @lit-material/text-field | mat-label → label, mat-hint → supporting-text, mat-error → error-text + error, appearance="outline" → variant="outlined", text prefix/suffix → prefix/suffix, icon prefix/suffix → leading-icon/trailing-icon slots. Numbers: lit-material-number-field. |
mat-select + mat-option | lit-material-select, lit-material-select-option | @lit-material/select | Single selection only: gap for multiple (consider lit-material-dual-list-selector or filter chips). No option groups. |
mat-autocomplete | lit-material-autocomplete | @lit-material/autocomplete | Options are data: [options]="[{ value, label }]"; free-text accepts values outside the list. |
mat-checkbox | lit-material-checkbox | @lit-material/checkbox | Label text goes in the label attribute; indeterminate as in Angular Material. |
mat-slide-toggle | lit-material-switch | @lit-material/switch | checked, change; thumb icons in the icon/selected-icon slots. |
mat-radio-group + mat-radio-button | lit-material-radio | @lit-material/radio | No group element (gap): radios with the same name form a group, with arrow-key movement; put them in a fieldset or role="radiogroup" for the group's label. |
mat-slider | lit-material-slider | @lit-material/slider | One thumb only: gap for a range slider (matSliderStartThumb/matSliderEndThumb). |
mat-chip, mat-chip-listbox, mat-chip-grid | lit-material-chip, lit-material-chip-input | @lit-material/chip | variant: assist, filter, input, suggestion. A chip grid with an input is lit-material-chip-input (values, change). No listbox element around chips. |
mat-button-toggle-group | lit-material-segmented-button-group, lit-material-segmented-button | @lit-material/segmented-button | multiselect on the group. |
MatDialog (service), mat-dialog-* | lit-material-dialog, lit-material-confirm-dialog | @lit-material/dialog, @lit-material/confirm | An element in your template, not a service: see below. confirm() returns a promise like afterClosed(). |
mat-menu + matMenuTriggerFor | lit-material-menu, lit-material-list-item | @lit-material/menu, @lit-material/list | anchor (an id) or show(trigger); items are lit-material-list-item interactive. Nested menus: gap. |
mat-tab-group, mat-tab | lit-material-tabs, lit-material-tab, lit-material-tab-panel | @lit-material/tabs | selectedIndex → selected; panels follow the tabs. Tab nav bar for routing: tabs with your router's links. |
mat-table (+ MatSort) | lit-material-data-table, lit-material-table | @lit-material/data-table, @lit-material/table | The data table sorts and selects by event (sort-change, selection-change): you reorder the rows, as with a server-side MatTableDataSource. No data source class. |
mat-paginator | lit-material-data-table-pagination | @lit-material/data-table | page, page-size, total, page-size-options; page-change event. |
mat-progress-bar | lit-material-linear-progress | @lit-material/progress | value/max or indeterminate; no buffer mode (gap). |
mat-progress-spinner, mat-spinner | lit-material-circular-progress | @lit-material/progress | indeterminate for mat-spinner. |
MatSnackBar (service) | lit-material-snackbar | @lit-material/snackbar | One element, show(message): see below. |
matTooltip (directive) | lit-material-tooltip | @lit-material/tooltip | An element with anchor set to the trigger's id. |
mat-sidenav, mat-drawer | lit-material-navigation-drawer, lit-material-drawer | @lit-material/navigation, @lit-material/drawer | Navigation drawer: variant="standard" (side) or "modal" (over); lit-material-adaptive-navigation switches drawer, rail and bar by width. |
mat-toolbar | lit-material-top-app-bar, lit-material-toolbar | @lit-material/top-app-bar, @lit-material/toolbar | App bar: variant small, center-aligned, medium, large. A toolbar of actions: lit-material-toolbar. |
mat-expansion-panel, mat-accordion | lit-material-accordion, lit-material-accordion-panel | @lit-material/accordion | expanded, toggle event; multi on the accordion. |
mat-stepper, mat-step | lit-material-stepper, lit-material-step | @lit-material/stepper | selected, linear, orientation; step completed/error. |
mat-datepicker (+ date range) | lit-material-date-picker, lit-material-date-range-picker | @lit-material/date-picker | A picker element (modal or docked) with ISO value strings; it isn't attached to an input: open it from a button or a field's trailing icon. No DateAdapter. |
mat-tree | lit-material-tree, lit-material-tree-item | @lit-material/tree | Nested items in markup, expanded/selected, multiple. No tree control or flattener. |
matBadge (directive) | lit-material-badge | @lit-material/badge | An element you position over the host yourself (value, max). |
mat-icon | lit-material-icon | @lit-material/icon | No icon font or ligatures: register SVG icons with registerIcons() and use name, or slot an <svg>. |
mat-card | lit-material-card | @lit-material/card | variant: elevated, filled, outlined; interactive or href for a clickable card. |
mat-list, mat-nav-list | lit-material-list, lit-material-list-item | @lit-material/list | Slots leading, trailing, overline, supporting-text; href for nav lists. Selection list: gap (checkboxes in trailing). |
mat-divider | lit-material-divider | @lit-material/divider | inset-start/inset-end; orientation="vertical". |
Services become elements
Dialogs. Put lit-material-dialog in the template and drive it with open; close fires however it closes (button, Escape, backdrop). For a yes/no question, confirm() from @lit-material/confirm creates a dialog, awaits the answer and removes it, much like MatDialog.open(…).afterClosed():
<lit-material-dialog [open]="editing" (close)="editing = false">
<div slot="headline">Edit profile</div>
<lit-material-text-field label="Name" formControlName="name" ngDefaultControl></lit-material-text-field>
<div slot="actions"><lit-material-button variant="text" (click)="editing = false">Cancel</lit-material-button></div>
</lit-material-dialog>
import { confirm } from "@lit-material/confirm";
if (await confirm({ headline: "Delete project?", confirmLabel: "Delete", destructive: true })) this.delete();
Snack bars. Keep one lit-material-snackbar in the app shell and a small service that calls its show(message):
@Injectable({ providedIn: "root" })
export class Snackbar {
private element?: HTMLElement & { show(message?: string): void };
attach(element: HTMLElement) { this.element = element as Snackbar["element"]; }
open(message: string) { this.element?.show(message); }
}
// In the app component, whose template has <lit-material-snackbar #snack></lit-material-snackbar>
private readonly snackbar = inject(Snackbar);
@ViewChild("snack") set snack(ref: ElementRef<HTMLElement>) { this.snackbar.attach(ref.nativeElement); }
Menus and tooltips. They're elements next to their trigger instead of directives on it: <lit-material-menu anchor="more"> opened with (click)="$any(menu).show()" on a template reference #menu, and <lit-material-tooltip anchor="save">Save changes</lit-material-tooltip> after id="save" on the button.
Gaps
- No
ControlValueAccessors and no Angular package: the patterns above, in your app. - No codemod: Angular Material templates use directives and services that don't map one to one; use the table.
- No multiple select, range slider, nested menus, selection list, chip listbox element or radio group element.
- No CDK equivalents (overlay, portal, a11y, drag and drop, virtual scroll); the data table virtualizes its own rows, and
@lit-material/sortablereorders lists. - Tables don't own data: no
MatTableDataSource, sort or paginator wiring; you sort and slice rows in response to events. - Not run inside an Angular app in this repository: the forms behaviour above was checked on the elements in Chromium, against how Angular's
DefaultValueAccessorworks.