Skip to content

Astro

Use the components in Astro two ways: React islands that mount in the browser, and zero-framework custom elements loaded by a page script.

Set up

npm create astro@latest my-site
cd my-site
npx astro add react            # only for React islands
npm install @kyralabs/m3exp-web @kyralabs/m3e-tokens @kyralabs/m3e-elements

@kyralabs/m3e-elements is needed only for its per-element entries (one import per element, below). The umbrella's @kyralabs/m3exp-web/elements registers all of them at once.

Load the styles in a layout

Import the CSS in the frontmatter of a layout every page uses. Astro bundles it and adds it to each page.

---
// src/layouts/Layout.astro
import '@kyralabs/m3exp-web/css/index.css';
import '@kyralabs/m3e-tokens/material-symbols.css';
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="color-scheme" content="light dark" />
    <title>My app</title>
  </head>
  <body>
    <slot />
  </body>
</html>

Markup with m3e-css classes, such as the link styled as a button below, can go straight into any .astro template: it is plain HTML and CSS, so it renders at build time and needs no JavaScript.

React islands

Write a React component and mount it with client:only="react". The components render in the browser only (DECISIONS D6), so do not use client:load, client:idle or client:visible: those render the component on the server first.

// src/components/LikeButton.tsx
import { useState } from 'react';
import { Icon, IconButton } from '@kyralabs/m3exp-web/react';

export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <IconButton variant="tonal" aria-label="Like" isSelected={liked} onChange={setLiked}>
      <Icon name="favorite" />
    </IconButton>
  );
}
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import LikeButton from '../components/LikeButton.tsx';
---

<Layout>
  <h1>Islands</h1>
  <a class="m3e-button m3e-button--tonal m3e-button--size-m" href="/elements/">Elements page</a>
  <LikeButton client:only="react" />
</Layout>

A client:only island leaves its spot empty until its script runs. To show something meanwhile, or to readers without JavaScript, give it static m3e-css markup in its fallback slot: <div slot="fallback">…</div>.

Custom elements, no framework

Import each element's entry in a page <script>: Astro bundles it, and the page loads only the elements it uses. Element events are typed, so event.detail needs no cast.

---
// src/pages/elements.astro
import Layout from '../layouts/Layout.astro';
---

<Layout>
  <h1>No framework</h1>
  <button id="more" type="button" class="m3e-icon-button" aria-label="More options">
    <span class="m3e-icon" aria-hidden="true">more_vert</span>
  </button>
  <m3e-menu anchor="more">
    <m3e-menu-item value="edit" icon="edit">Edit</m3e-menu-item>
    <m3e-menu-item value="share" icon="share">Share</m3e-menu-item>
  </m3e-menu>
  <m3e-loading-indicator contained aria-label="Loading"></m3e-loading-indicator>
</Layout>

<script>
  import '@kyralabs/m3e-elements/ripple.js';
  import '@kyralabs/m3e-elements/menu.js';
  import '@kyralabs/m3e-elements/loading-indicator.js';

  document.querySelector('m3e-menu')?.addEventListener('action', (event) => {
    console.log('picked', event.detail.value);
  });
</script>

Elements render their markup in the browser when their entry loads. Until then their children are data (the menu items above stay hidden), and the page's static parts are already there. ripple.js adds the press ripple to every plain-HTML button, chip, card and list item on the page; see ripple for plain HTML.

Keep pages light

  • Import only the components a page shows: the token stylesheets plus one @kyralabs/m3e-css/<component>.css each (install @kyralabs/m3e-css), as in the overview.
  • Subset the icon font with m3e-icons; see fonts and icons.

This site is an Astro site built this way; its source is apps/site in the repository.