Skip to contentEnma

Build with React (web)

For Next.js, Vite, Remix, Astro islands and Electron. The components are plain React 18+ with no runtime dependencies; styling is CSS variables, so they work with server rendering and any bundler.

Install

After the one-time setup:

sh
pnpm add enma@npm:enma-design

enma/react has the same component names, props and class names (e-btn, e-stat, e-card) as the API contract. Use them as they are; don't redesign them per project.

Next.js (App Router)

tsx
// app/layout.tsx
import 'enma/css';
import 'enma/fonts/ember';
import './globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" data-enma="ember" data-theme="dark">
      <body className="e-canvas">{children}</body>
    </html>
  );
}
  • Set data-enma and data-theme on <html> on the server, so the first paint is already correct (no flash).
  • Components that use browser APIs (count-up, bar growth, keyboard handling) are already marked 'use client'. Server components can render Card, Badge, Table, ListGroup and Avatar directly.
  • Prefer next/font? Point the theme's font variable at it in your global CSS: :root { --font-geist: var(--font-geist-loaded), system-ui, sans-serif; }.

Vite and other single-page apps

html
<!-- index.html -->
<html lang="en" data-enma="ember" data-theme="dark">
  <body class="e-canvas">
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
tsx
// src/main.tsx
import 'enma/css';
import 'enma/fonts/ember';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

Or wrap the app in <EnmaRoot theme="ember"> instead of setting attributes on <html>.

A finished screen

tsx
import { Bars, Button, Card, ListGroup, Stat, TabBar } from 'enma/react';

const week = [38, 52, 44, 70, 58, 82, 96];

export function TodayScreen({ onStart }: { onStart: () => void }) {
  return (
    <main className="screen">
      <header className="screen__top">
        <p className="e-eyebrow">Thursday</p>
        <h1 className="e-title">Push day</h1>
      </header>
      <Card variant="hero" padding="lg">
        <Stat size="hero" label="Weekly volume" value={12480} unit="kg" delta="+8%" />
        <Bars data={week} labels={['M', 'T', 'W', 'T', 'F', 'S', 'S']} style={{ marginTop: 18 }} />
      </Card>
      <ListGroup>
        <ListGroup.Row title="Bench press" meta="4 × 8 · 80 kg" />
        <ListGroup.Row title="Incline press" meta="3 × 10 · 26 kg" />
      </ListGroup>
      <Button variant="primary" size="lg" block onClick={onStart}>
        Start workout
      </Button>
      <TabBar
        defaultActive="today"
        items={[
          { key: 'today', label: 'Today', icon: 'home' },
          { key: 'stats', label: 'Stats', icon: 'chart' },
          { key: 'you', label: 'You', icon: 'user' },
        ]}
      />
    </main>
  );
}
css
.screen {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  max-width: 480px;
  margin: 0 auto;
}
.screen__top {
  padding: var(--space-1) var(--space-1) 0;
}
  • className and style on components are for layout only (margins, grid placement, width). Never restyle a component's colors, radius or type from outside; if it needs to look different, change the system.
  • One hero, one primary button, one highlighted bar. That screen has all three, once each.

Light/dark toggle (only if the product needs one)

Store the choice, apply it before paint, and fall back to the system setting:

html
<!-- first thing in <head> -->
<script>
  try {
    var m = localStorage.getItem('enma-mode');
    if (m !== 'light' && m !== 'dark')
      m = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    document.documentElement.dataset.theme = m;
  } catch (e) {}
</script>
tsx
export function setMode(mode: 'light' | 'dark' | 'system') {
  const m =
    mode === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : mode;
  document.documentElement.dataset.theme = m;
  try {
    mode === 'system' ? localStorage.removeItem('enma-mode') : localStorage.setItem('enma-mode', m);
  } catch {}
}

Styling your own UI

Plain CSS or CSS modules: use only --e-*, --space-*, --control-*, --duration-* and --easing-*.

css
.panel {
  background: var(--e-surface);
  border-radius: var(--e-r-lg);
  box-shadow: var(--e-shadow-card);
}

Tailwind CSS v4: map Enma into the theme once, and utilities follow the active theme automatically.

css
@import 'tailwindcss';
@import 'enma/css';
@import 'enma/tailwind.css';

Then write bg-surface text-ink rounded-lg font-display. Every role is mapped (bg-hero, text-on-hero, ring-focus and the rest). Tailwind's spacing scale is already 4px based (p-4 is 16px, the same as --space-4). Don't use Tailwind's built-in palette (bg-blue-500) anywhere.

Missing pieces

  • Dialog, Sheet, Menu, Popover, Tooltip, Combobox: Radix UI primitives or React Aria, styled with Enma tokens (--e-surface, --e-r-lg, --e-shadow-card). They bring focus trapping, Escape handling and ARIA for free.
  • Command palette: cmdk, styled the same way, on Cmd/Ctrl+K. The analytics sample has one built on a native <dialog>.
  • Charts beyond Bars, LineChart, Sparkline and Donut: any chart library, with the series color from --e-accent (highlight) and a 10% --e-ink tint for the rest.
  • Data fetching, routing and state are not Enma's job; any stack works.

↑ ↓ to move, Enter to open, Esc to close

Keyboard shortcuts

Open the command palette
Ctrl then K
Search
/
Show keyboard shortcuts
?
Close the top overlay
Esc
Go to Home
G then H
Go to Themes
G then T
Go to Playground
G then P
Go to Components
G then C
Go to Guides
G then G
Go to Samples
G then S