MDC

Get Started

@sandlada/mdc ships Material Design 3 and Material Expressive as framework-agnostic web components. Install the library, load the design tokens, register the elements, then use plain <mdc-*> tags anywhere.

Install

Add the component library and the CSS design tokens it is styled with. Both are plain ESM packages with no build step required.

npm i @sandlada/mdc @sandlada/material-design-css

Load the design tokens

Every component is styled through --md-sys-* custom properties supplied by@sandlada/material-design-css. Import the preset once, then choose a prebuilt color scheme.

@import '@sandlada/material-design-css/preset.css';

/* Pick a prebuilt color scheme (ships light and dark). */
@import '@sandlada/material-design-css/prebuilt-colors/tonal-spot/h0-2025.css';

/* Optional: Tailwind utilities such as bg-surface / text-on-surface. */
@import '@sandlada/material-design-css/color/tw.css';

Toggle the dark attribute on <html> to switch themes, and thelow-contrast / high-contrast attributes to adjust contrast.

document.documentElement.toggleAttribute('dark', isDark);

Register the components

Importing a component module registers its custom element as a side effect. Each component is its own entry point under @sandlada/mdc/components/<name>/index, so import exactly the elements you use - nothing else is pulled into your bundle.

// Importing a component module registers its <mdc-*> element.
import '@sandlada/mdc/components/button/index';
// Import every element you need - each component is its own entry point.
import '@sandlada/mdc/components/button/index';
import '@sandlada/mdc/components/icon/index';
import '@sandlada/mdc/components/text-field/index';

Use the components

With the elements registered, use them like any other HTML tag - in plain HTML or inside any framework template.

<mdc-button variant="filled"><span>Save</span></mdc-button>
<mdc-button variant="outlined"><span>Cancel</span></mdc-button>

Live example

Code
<mdc-button variant="filled"><span>Filled</span></mdc-button>
<mdc-button variant="filled-tonal"><span>Filled tonal</span></mdc-button>
<mdc-button variant="outlined"><span>Outlined</span></mdc-button>
<mdc-button variant="text"><span>Text</span></mdc-button>

Configure global behaviors

Ripple, focus ring, and elevation behaviors can be tuned once for the whole document. Attach the provider before your app renders; it is optional, and components fall back to sensible defaults without it.

import { GlobalMDCContextProvider } from '@sandlada/mdc/utils/context-provider';

// Attach once, before your app renders.
GlobalMDCContextProvider.attach();
GlobalMDCContextProvider.setConfig({
    ripple: { disableHoverStateLayer: false },
    focusRing: {},
    elevation: { disabled: false },
});

Framework notes

Web components work natively everywhere. A little configuration tells a compiler to leave mdc-* tags alone rather than treating them as unknown components.

Vue

// vite.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => tag.startsWith('mdc-'),
                },
            },
        }),
    ],
});

Angular

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}

React

React 19 renders custom elements natively. On older versions, set properties through a ref instead of passing them as attributes.

Next steps