Skip to content

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.