advanced-material-web is a set of custom elements, so it already runs in any framework with one
line of configuration — see the main README's Frameworks section. The
subpaths below exist for the parts a framework can't do well on its own: typed props, real event
bindings, two-way binding, and server rendering — no separate install, same package.
Everything here is generated from the component sources during npm run build:all — the
adapters cannot drift from the elements, and none of it is committed.
| Subpath | Framework | Two-way binding | SSR |
|---|---|---|---|
advanced-material-web/react |
React 18+ | controlled props | declarative shadow DOM via @stencil/ssr (/react/ssr) |
advanced-material-web/vue |
Vue 3.5+ | v-model |
automatic (Nuxt), via the hydrate module |
advanced-material-web/angular |
Angular 19+ | ControlValueAccessor |
tags only, no pre-rendered shadow content |
advanced-material-web/svelte |
Svelte 5 | bind:value / bind:checked |
tags only, no pre-rendered shadow content |
import { useState } from 'react';
import { MaterialTextfield, MaterialButton, MaterialSelect, MaterialOption } from 'advanced-material-web/react';
export function ContactForm() {
const [email, setEmail] = useState('');
return (
<form>
<MaterialTextfield
name="email"
type="email"
label="Email"
required
value={email}
onValueChange={(e) => setEmail(e.detail.value)}
/>
<MaterialSelect name="country" label="Country">
<MaterialOption value="us">United States</MaterialOption>
<MaterialOption value="de">Germany</MaterialOption>
</MaterialSelect>
<MaterialButton type="submit" label="Save" />
</form>
);
}- Props are camelCase (
helpText,leadingIcon) and are set as DOM properties, so arrays and objects (options,commands) pass through without serialising. - Events are
on+ the event name (onValueChange,onMaterialStepChange) and receive the realCustomEvent— the payload is onevent.detail. - Refs give you the custom element itself, so
@Method()members are directly callable:ref.current?.reportValidity().
Server rendering (Next.js, Remix, Vite) goes through advanced-material-web/react/ssr and
@stencil/ssr:
// next.config.mjs
import stencilSSR from '@stencil/ssr/next';
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default stencilSSR({
module: import('advanced-material-web/react/ssr'),
from: 'advanced-material-web/react',
hydrateModule: import('advanced-material-web/hydrate'),
serializeShadowRoot: 'declarative-shadow-dom',
})(nextConfig);Vite/Remix use the stencilSSR plugin and webpack uses StencilSSRWebpackPlugin with the same
options — see the @stencil/ssr docs. Without that
plugin the components are client components (they carry 'use client'), which works but renders
nothing until hydration.
<script setup lang="ts">
import { ref } from 'vue';
import { MaterialTextfield, MaterialSelect, MaterialOption, MaterialButton } from 'advanced-material-web/vue';
const email = ref('');
const country = ref('us');
</script>
<template>
<form>
<MaterialTextfield v-model="email" name="email" type="email" label="Email" required />
<MaterialSelect v-model="country" name="country" label="Country">
<MaterialOption value="us">United States</MaterialOption>
<MaterialOption value="de">Germany</MaterialOption>
</MaterialSelect>
<MaterialButton type="submit" label="Save" />
</form>
</template>v-model works on the controls that carry a single value — the text-like fields, select,
autocomplete, radio-group, slider (value) and checkbox / switch (checked).
Server rendering (Nuxt) needs no configuration: each component picks its implementation at import
time — in the browser it binds the custom element, on the server it renders through
advanced-material-web/hydrate into declarative shadow DOM.
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
import { MaterialTextfield, MaterialSelect, MaterialOption, MaterialButton } from 'advanced-material-web/angular';
@Component({
selector: 'app-contact',
imports: [ReactiveFormsModule, MaterialTextfield, MaterialSelect, MaterialOption, MaterialButton],
template: `
<form [formGroup]="form" (ngSubmit)="save()">
<material-textfield formControlName="email" label="Email" type="email" required />
<material-select formControlName="country" label="Country">
<material-option value="us">United States</material-option>
<material-option value="de">Germany</material-option>
</material-select>
<material-button type="submit" label="Save" />
</form>
`,
})
export class ContactComponent {
form = new FormGroup({
email: new FormControl(''),
country: new FormControl('us'),
});
save() { console.log(this.form.value); }
}- Selectors are the tag names (
material-textfield), so templates read like plain HTML while still being type-checked against the component's inputs. No NgModule. - Change detection is detached on every wrapper; property writes run outside
NgZone. DIRECTIVESexports every wrapper as an array, for importing them all at once.
Angular SSR renders the components as their tags with projected content, and they upgrade on the
client — Stencil has no first-class Angular hydration path, but advanced-material-web/hydrate
is there if you want to pre-render the shadow content yourself.
<script>
import { MaterialTextfield, MaterialSelect, MaterialOption, MaterialButton } from 'advanced-material-web/svelte';
let email = $state('');
let country = $state('us');
</script>
<form>
<MaterialTextfield bind:value={email} name="email" type="email" label="Email" required />
<MaterialSelect bind:value={country} name="country" label="Country">
<MaterialOption value="us">United States</MaterialOption>
<MaterialOption value="de">Germany</MaterialOption>
</MaterialSelect>
<MaterialButton type="submit" label="Save" />
</form>Ships as .svelte source — your bundler compiles it, so there is no prebuilt runtime pinned to a
Svelte version. SSR renders the tags with no pre-rendered shadow content, same as Angular.
- Events carry the real
CustomEvent, payload onevent.detail(or$event.detailin Angular templates) — nothing framework-specific is invented. - The components stay form-associated: inside a plain
<form>they post real values and take part in constraint validation, wrapped or not. - Attributes, events and methods for every component live in
src/components/<tag>/readme.md.
Driven by VALUE_ELEMENTS / CHECKED_ELEMENTS in stencil.config.ts, shared by all four targets:
the text-like fields, select, autocomplete, radio-group and slider bind value;
checkbox and switch bind checked.
material-date-range-field and material-transfer are deliberately excluded — they emit
{start, end} and {values} respectively, so there is no single value to bind. Handle them with an
explicit valueChange listener.
dist-hydrate-script produces advanced-material-web/hydrate, which renders any component to HTML
off the DOM:
import { renderToString } from 'advanced-material-web/hydrate';
const { html } = await renderToString('<material-button label="Save" variant="filled"></material-button>', {
serializeShadowRoot: 'declarative-shadow-dom',
});React and Vue wire that up for you (above). Angular and Svelte render the tags and let the elements upgrade on the client; the hydrate module is there if you want to pre-render yourself.
Generated into dist/ and published with the package:
| File | Format | Consumers |
|---|---|---|
dist/html-data.json |
VS Code custom data | attribute completion and hover docs in plain HTML and Django templates |
dist/web-types.json |
JetBrains web-types | the same, in WebStorm / PyCharm / IntelliJ |
dist/custom-elements.json |
Custom Elements Manifest | Storybook, doc generators, most editor plugins |
dist/docs.json |
Stencil docs-json | the source the two above are generated from |
custom-elements.json and web-types.json are advertised through the customElements and
web-types fields in package.json, so tools that look for them find them without configuration.
JetBrains IDEs pick web-types up automatically.
VS Code needs one setting:
// .vscode/settings.json
{
"html.customData": ["./node_modules/advanced-material-web/dist/html-data.json"]
}npm run build (core package):
- The four stylesheets under
css/: Tailwind compilestheme.cssandtokens.css,scripts/build-tailwind-preset.mjspublishestailwind.cssuncompiled (consumers' own Tailwind reads its@themedirectives), andscripts/build-material-css.mjsbundlesmaterial.css. stencil buildemitsdist/,loader/,hydrate/, the readmes, the tooling JSON, and the four framework adapters' generated source intoadapters/*/src.scripts/patch-custom-element-types.mjsfixes the custom-elements typings (below).scripts/build-web-types.mjsderivesweb-types.jsonfromdocs.json.scripts/build-cdn.mjsbundles the single-file CDN build.
npm run build:adapters then compiles each framework adapter (tsc for React and Vue, ng-packagr
for Angular; Svelte ships source, so there's nothing to compile). npm run build:all does both, in
order — this is what npm publish runs, through prepublishOnly.
npm start sets MATERIAL_WRAPPERS=0 so watch rebuilds skip adapter codegen.
The typings patch. Stencil emits interface X extends Components.X, HTMLElement {} for each
custom element. Where a @Prop() shadows something HTMLElement already has — ariaLabel (ARIA
reflection makes it a real DOM property typed string | null, while a Stencil prop is
string | undefined), plus prefix and focus — the two bases conflict and the interface no
longer satisfies HTMLElement, which every generated React adapter requires. The patch script
applies the same Omit<…> that Stencil already uses for methods in dist/types, computed from the
real HTMLElement members rather than a hand-kept list. It also re-exports Components and JSX
from dist/components/index.d.ts, which auto-define-custom-elements leaves out.
The Svelte target is ours. @stencil/svelte-output-target is still 0.0.3 and calls the Svelte
3/4 compiler API, which Svelte 5 rejects outright. scripts/svelte-output-target.mjs generates
Svelte 5 source instead — the normal way Svelte libraries publish, so there is no build step and no
compiled runtime pinned to one Svelte version.