React with Vite
Set up a React app with Vite, load the styles once, and use the components: React Aria for behavior and accessibility, Motion for springs.
Create the app
Any React 18.3+ or 19 setup works. With Vite's React template (TypeScript or not):
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install @kyralabs/m3exp-web @kyralabs/m3e-tokens
Delete the template's src/index.css and src/App.css and their
imports: they restyle the body and every button, which the Material theme already does.
Load the styles
Import the stylesheet and the icon font once, in src/main.tsx. They set the
fonts, the tokens, the baseline light and dark theme and the classes every component renders.
import '@kyralabs/m3exp-web/css/index.css';
import '@kyralabs/m3e-tokens/material-symbols.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Use components
Import components from @kyralabs/m3exp-web/react (or
@kyralabs/m3e-react if you install it directly). Props follow React Aria:
onPress instead of onClick, is… for states such as
isSelected and isDisabled. Icon-only controls need an
aria-label.
import { useState } from 'react';
import { Button, Card, Icon, IconButton, LoadingIndicator } from '@kyralabs/m3exp-web/react';
export default function App() {
const [saving, setSaving] = useState(false);
const [liked, setLiked] = useState(false);
return (
<Card variant="outlined" style={{ padding: 'var(--m3e-sys-space-300)' }}>
<h1>Hello, Expressive</h1>
<Button
variant="filled"
size="m"
icon={<Icon name="save" />}
onPress={() => setSaving((s) => !s)}
>
{saving ? 'Saving…' : 'Save'}
</Button>
<IconButton aria-label="Like" isSelected={liked} onChange={setLiked}>
<Icon name="favorite" />
</IconButton>
{saving && <LoadingIndicator aria-label="Saving" />}
</Card>
);
}
npm run dev
Each component is tree-shaken on its own, so the app pays only for what it imports. Size
budgets guard this in the library's CI: { Button } alone stays within 16 kB,
min and brotli, React excluded.
Before you ship
Ship only your icons
material-symbols.css loads the whole Material Symbols font, several megabytes.
Subset it to the icons you use, including the ones components draw themselves (such as the date
picker's arrows), and import the generated CSS instead:
npm install --save-dev @kyralabs/m3e-icons
npx m3e-icons subset --names save,favorite --out src/icons/icons.woff2 --css src/icons/icons.css
import '@kyralabs/m3exp-web/css/index.css';
import './icons/icons.css'; // instead of material-symbols.css
Fonts and icons covers the other font profiles and the subset options.
Load less CSS
css/index.css holds every component. To load only the components you use, import
the token stylesheets and one m3e-css file per component, as shown in the
overview; install @kyralabs/m3e-css directly for
that.
Your brand color
Generate a theme from a seed color at build time with the m3e-theme CLI or at
runtime with createTheme; see theming.
Client-side only
The components render in the browser only (DECISIONS D6): Vite single-page apps need nothing special. For Next.js and other server-rendering frameworks, see server rendering.