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:
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)
// 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-enmaanddata-themeon<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 renderCard,Badge,Table,ListGroupandAvatardirectly. - 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
<!-- 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>
// 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
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>
);
}
.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;
}
classNameandstyleon 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:
<!-- 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>
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-*.
.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.
@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,SparklineandDonut: any chart library, with the series color from--e-accent(highlight) and a 10%--e-inktint for the rest. - Data fetching, routing and state are not Enma's job; any stack works.