v 1.0.0
UI library for SaaS applications. e-ui provides a design system (CSS) and interactive custom elements (JavaScript) that work alongside EHTML — a declarative, no-build-step HTML framework.
- CSS:
static/css/e-ui.css— design tokens, utilities, typography, layout, forms, and component styles - JS:
static/js/e-ui/— custom elements and EHTML template extensions - Examples:
static/html/— runnable demo pages (start the dev server withnpm run server)
e-ui is designed to be used with EHTML. EHTML activates templates, binds data, and runs actions; e-ui components listen for EHTML's ehtml:activated event before initializing. Import e-ui modules after #ehtml/main so custom elements are registered when EHTML activates the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<script type="importmap">
{
"imports": {
"#ehtml/": "/js/ehtml/",
"#ehtml/main": "/js/ehtml/main.js",
"#e-ui/": "/js/e-ui/"
}
}
</script>
<script type="module">
import '#e-ui/e-toast.js'
import '#e-ui/e-dialog.js'
import '#ehtml/main'
</script>
<link rel="stylesheet" href="/css/e-ui.css">
</head>
<body>
<!-- your markup -->
</body>
</html>npm run serverOpen http://localhost:8004/html/index.html for the example gallery.
Override CSS variables on :root to theme an app (for example, set --e-primary from your brand color):
:root {
--e-primary: #3551A4;
--e-bg: #f6f7fb;
--e-surface-bg: #ffffff;
--e-spacing-sm … --e-spacing-3xl;
--e-font-size-xs … --e-font-size-3xl;
--e-sidebar-collapsed: 4.5rem;
--e-sidebar-expanded: 20rem;
}Apply spacing, flex, color, and sizing without custom CSS. Works on any element or [is^="e-"]:
<div is="e-stack" data-gap="md" data-padding="lg" data-align-items="center">
<span is="e-muted" data-font-size="sm">Subtitle</span>
</div>Common utilities: data-padding, data-margin, data-gap, data-flex-direction, data-justify-content, data-align-items, data-width (xs–xl, full), data-font-size, data-font-weight, data-color, data-background, data-border-radius, data-columns.
Styled text elements use the is attribute on native HTML tags. Only e-ui.css is required — no JS imports.
Live demo: typography-layout.html · Screenshot: typography.png
Use is="e-h" on any heading level. Size comes from the tag (h1–h6).
<h1 is="e-h">Page title</h1>
<h2 is="e-h">Section title</h2>
<h3 is="e-h">Subsection</h3>
<h6 is="e-h" data-color="muted" data-text-case="upper">Eyebrow label</h6><p is="e-p">Body copy uses the typography font stack and relaxed line height.</p>
<blockquote is="e-quote">
A blockquote for testimonials or callouts.
</blockquote>
<pre is="e-pre">const greeting = 'Hello'</pre>is value |
Purpose |
|---|---|
e-text |
Default body text color |
e-muted |
Secondary / de-emphasized text |
e-light |
Text on dark backgrounds |
e-bold |
Bold emphasis |
e-caption |
Small uppercase label |
e-error, e-danger |
Error message color |
e-helper |
Helper text below fields (block) |
<span is="e-text">Default text</span>
<span is="e-muted">Muted description</span>
<span is="e-bold">Important</span>
<span is="e-caption">Section label</span>
<span is="e-error">This field is required</span>
<span is="e-helper">Shown below an input</span><span is="e-badge">3</span>
<span is="e-tag">Draft</span>
<span is="e-chip">Removable chip <img src="/images/close.svg" alt=""></span>span[is="stack"] is an inline vertical stack for compact multi-line labels:
<span is="stack">
<span is="e-bold">Jane Doe</span>
<span is="e-muted">Instructor</span>
</span><p is="e-p">Run <code is="e-code">npm run server</code> locally.</p>
<p is="e-p">Search for <mark is="e-mark">keyword</mark> in results.</p><a is="e-link" href="#">Text link</a>
<a is="e-link" data-underlined href="#">Underlined link</a><hr is="e-divider">
<hr is="e-divider" data-dashed>
<hr is="e-divider" data-primary-color>
<hr is="e-divider-text" data-text="Or continue with">Flex, grid, and page-structure primitives. Combine with utility attributes (data-gap, data-padding, data-width, etc.).
Live demo: typography-layout.html · Screenshot: layout.png
is value |
Element | Behavior |
|---|---|---|
e-stack |
div |
Column flex container |
e-row |
div |
Row flex container (stacks on mobile unless data-keep-flex-direction-row-in-mobile) |
e-grid |
div |
3-column grid (1 column below 980px) |
e-box |
div |
Plain block wrapper |
e-spacer |
div |
Flex grow spacer |
<div is="e-stack" data-gap="md">
<div is="e-row" data-gap="sm" data-justify-content="space-between">
<span is="e-bold">Title</span>
<div is="e-spacer"></div>
<button data-primary>Action</button>
</div>
</div>
<div is="e-grid" data-gap="lg" data-columns="2">
<div is="e-card" data-padding="lg">Column A</div>
<div is="e-card" data-padding="lg">Column B</div>
</div><div is="e-card" data-padding="lg">Bordered card with shadow</div>
<div is="e-panel" data-padding="lg">Full-width elevated panel</div>
<div is="e-info" data-padding="md">Muted info notice box</div><header is="e-header"><!-- fixed mobile header --></header>
<main is="e-main" data-centralized>
<section is="e-section"><!-- content --></section>
</main>
<footer is="e-footer" data-justify-content="center">© 2026</footer>data-centralized on e-main applies max-width and horizontal padding from --e-main-max-width.
These patterns need only e-ui.css. They are separate from typography and layout primitives above.
Live demo: forms-buttons.html · Screenshot: forms-buttons.png
<button data-primary>Solid</button>
<button data-primary data-fill="outlined">Outlined</button>
<button data-primary data-fill="danger">Danger</button>
<button is="e-with-icon" title="Add">
<img src="/images/plus.svg" alt="">
</button>
<a is="e-link-button" href="#">Link button</a>
<a is="e-link-button" data-variant="outline" href="#">Outline link button</a>Live demo: forms-buttons.html · Screenshot: forms-buttons.png
<form is="e-form" data-width="sm">
<label>
Email
<input type="email" name="email" required>
</label>
<label data-flex-direction="row">
<input type="checkbox" name="remember">
Remember me
</label>
<label>
Role
<select name="role"><option>Admin</option></select>
</label>
<label>
Notes
<textarea name="notes" rows="3"></textarea>
</label>
<button data-primary type="submit">Save</button>
</form>Use data-no-style on e-form to skip the card-like wrapper. Group fields with fieldset[is="e-fieldset"] and legend[is="e-legend"].
File inputs inside e-form pick up drag-and-drop styling automatically when generated by the e-file-upload template (see Custom elements).
<table is="e-table">
<thead>
<tr><th>Name</th><th>Role</th></tr>
</thead>
<tbody>
<tr><td>Alice</td><td>Admin</td></tr>
</tbody>
</table><ul is="e-list" data-type="bullet" data-divided="true">
<li is="e-list-item">Item one</li>
<li is="e-list-item" data-interactive="true">Clickable item</li>
</ul>Live demo: forms-buttons.html · Screenshot: user-avatar.png
Circular avatar with initials or background image. With EHTML, data-text is evaluated into the element on activation.
<div
is="e-user-avatar"
data-size="md"
style="background-color: #3551A4;"
data-text="JD">
</div>
<div
is="e-user-avatar"
data-size="md"
style="background: url(/avatar.jpg) center center / cover no-repeat; color: transparent;"
data-text="JD">
</div>Size with data-size, data-width, or data-height (3xs–3xl).
Live demo: forms-buttons.html · Screenshot: forms-buttons.png
e-tooltip is styled by CSS only — wrap the trigger element and set data-tip:
<e-tooltip data-tip="Copy URL" data-direction="to-right">
<img src="/images/copy.svg" alt="Copy">
</e-tooltip>Use the e-scrollable tag for overflow containers (also used internally by multiselect dropdown):
<e-scrollable data-show-scrollbar data-padding="sm">
<!-- long content -->
</e-scrollable>Live demo: chips-kbd.html · Screenshot: forms-buttons.png
kbd[is="e-kbd"] is styled by CSS. To bind a global shortcut, import e-kbd.js — see Custom elements.
<kbd is="e-kbd">Ctrl + K</kbd>
<kbd is="e-kbd" data-absolute>⇧ + S</kbd>These require JS module imports (before #ehtml/main) and EHTML activation. Most expose data-action, data-onopen, or data-onclose strings evaluated by EHTML.
| Module | Element | Description |
|---|---|---|
e-toast.js |
<e-toast> |
Toast notifications |
e-dialog.js |
dialog[is="e-dialog"] |
Modal dialogs |
e-sidebar.js |
<e-sidebar> |
App sidebar |
e-tab.js |
<e-tabs>, <e-tab> |
Tab navigation |
e-kbd.js |
kbd[is="e-kbd"] |
Keyboard shortcuts via data-action |
e-kbd-graph.js |
<e-kbd-graph> |
Arrow-key focus graph |
e-selectable-chip.js |
<e-selectable-chip> |
Toggle chip |
e-autoclick-button.js |
button[is="e-autoclick"] |
Auto-click on activation |
e-file-upload-template.js |
<template is="e-file-upload"> |
File upload drop zone |
e-multiselect-dropdown-template.js |
<template is="e-multiselect-dropdown"> |
Searchable multiselect |
e-week-picker-template.js |
<template is="e-week-picker"> |
Week navigation |
e-multiselect-dropdownrequirese-kbd.jsto be imported before its template module.
Live demo: dialog-toast.html · Screenshot: toast.png
<e-toast
data-type="success"
data-position="top-right"
data-hide-after-n-seconds="5"
data-close-icon="/images/close.svg"
></e-toast>document.querySelector('e-toast').open('Saved successfully!')API: .open(content?, delay?), .close()
| Attribute | Values |
|---|---|
data-type |
success, error, warning, info |
data-position |
top-left, top-right, bottom-left, bottom-right |
data-hide-after-n-seconds |
Auto-close delay |
data-icon, data-close-icon |
Image URLs |
Screenshot: dialog.png
<dialog id="my-dialog" is="e-dialog" data-size="normal" data-close-icon="/images/close.svg">
<div data-padding="lg">
<h2 is="e-h">Confirm</h2>
<p is="e-p">Are you sure?</p>
<button data-primary onclick="document.querySelector('#my-dialog').close()">OK</button>
</div>
</dialog>API: .showModal(), .close() — backdrop click and Escape close; body scroll is locked while open.
| Attribute | Description |
|---|---|
data-close-icon |
Close button image URL |
data-open-on-load |
Open when activated |
data-onopen, data-onclose |
EHTML action strings |
data-size |
small, normal, large, full |
Live demo: sidebar-tabs.html · Screenshot: sidebar.png
<div is="overlay"></div>
<header is="e-header">Mobile header</header>
<e-sidebar
data-side="left"
data-mobile-header='header[is="e-header"]'
data-mobile-menu-icon="/images/menu-icon.svg"
data-mobile-body-overlay="div[is='overlay']"
>
<header><!-- logo --></header>
<nav>
<a href="#" data-selected="true"><img src="/icon.svg"><span>Home</span></a>
</nav>
<footer><!-- user --></footer>
</e-sidebar>API: .open(), .close()
Live demo: sidebar-tabs.html · Screenshot: tabs.png
<e-tabs data-apply-hash-navigation>
<e-tab data-title="Overview"><p is="e-p">Tab 1</p></e-tab>
<e-tab data-title="Settings"><p is="e-p">Tab 2</p></e-tab>
</e-tabs>API: e-tabs.selectTab(index)
Import e-kbd.js. Text content defines the shortcut; data-action runs EHTML actions on match.
Live demo: chips-kbd.html · Screenshot: kbd.png
<kbd is="e-kbd" data-action="document.querySelector('e-toast').open('Shortcut!')">
Ctrl + K
</kbd>| Attribute | Description |
|---|---|
data-action |
EHTML action string |
data-trigger-in-inputs |
Fire while focus is in an input |
data-absolute |
Badge positioning |
<e-kbd-graph data-first-element="button:first-of-type">
<button data-right="button:nth-of-type(2)" data-click-on-enter>One</button>
<button data-left="button:first-of-type">Two</button>
</e-kbd-graph>Link focus with data-left, data-right, data-up, data-down.
Live demo: chips-kbd.html · Screenshot: chips.png
<e-selectable-chip data-value="jazz" data-selected="true">Jazz</e-selectable-chip>API: .select(), .unselect()
<button is="e-autoclick" data-condition-to-click="true" data-primary>Submit</button>Clicks itself once when EHTML activates and the condition is truthy.
Templates use data-internal-state for dynamic data and are replaced with interactive UI on activation.
Live demo: file-upload.html · Screenshot: file-upload.png
<template
is="e-file-upload"
data-name="avatar"
data-accept="image/png, image/jpeg"
data-icon-src="/images/upload-image.svg"
data-label-text="Profile photo"
data-action-text="Click or drag a file here"
data-details-text="PNG or JPG up to 2 MB"
data-max-size-in-mb="2"
data-show-errors-in-toast
data-set-height
></template>Use multiple, data-max-number-of-files, and data-required as needed. When using data-show-errors-in-toast, define window.showErrorToast(message) in your page script.
Live demo: multiselect-dropdown.html · Screenshot: multiselect.png
<template
is="e-multiselect-dropdown"
data-internal-state="${{
values: ['Jazz', 'Rock', 'Classical'],
selectedValues: ['Jazz'],
valueDisplayFunction: function (internalState, value) { return value }
}}"
data-name="genres"
data-label="Select genres"
data-selected-chip-close-icon="/images/close.svg"
>
<template></template>
</template>Add data-always-on to keep the checkbox list visible without a search-triggered dropdown.
<template
is="e-week-picker"
data-prev-icon="/images/chevron-left.svg"
data-next-icon="/images/chevron-right.svg"
data-on-week-change="/* EHTML actions */"
></template># Start static file server
npm run server
# Sync EHTML source from sibling repo
npm run ehtml:update
# Sync nodes.js helpers
npm run nodes:updateMIT