Lightweight JavaScript component framework built with HTML custom elements, with an ergonomics layer that makes them easier to author and nicer to use. Ships a library of UI elements — buttons, inputs, tabs, lists, windows, wizards — plus data collection classes for binding them to live data sources.
On top of the standard custom element API, Elation layers a typed attribute system (int, float, boolean, vector2, etc.) with automatic attribute ↔ property coercion, lifecycle hooks (init / create / render) that fire at the right time relative to child parsing, inheritance via elation.elements.base, and an elation.elements.create() constructor that accepts an args object in place of the usual document.createElement + setAttribute dance.
elation.elements.ui— elements for building UI: buttons, inputs, tabs, lists, layouts, and feedback indicators.elation.elements.collection— data collection classes for backing lists and grids with live-updating data, with adapters for REST APIs, IndexedDB, and SQLite.elation/elation.utils/elation.html— core utilities the framework is built on.
Use them in markup:
<ui-tabs>
<ui-tab label="One">Content for tab one</ui-tab>
<ui-tab label="Two">Content for tab two</ui-tab>
</ui-tabs>Or from JavaScript, either via the regular DOM API:
const tabs = document.createElement('ui-tabs');
document.body.appendChild(tabs);…or via elation.elements.create(), which takes an args object mirroring the element's attributes (and can append in the same step):
const input = elation.elements.create('ui-input', {
placeholder: 'Name',
value: 'Ada',
append: document.body,
});The args table on each class page lists the keys accepted by elation.elements.create(); each key also maps to an HTML attribute of the same name.
See the demo gallery for live examples of every element.
Register a class with elation.elements.define(name, class). Dots in the name become dashes for the HTML tag: 'ui.counter' registers <ui-counter> and exposes the class at elation.elements.ui.counter.
elation.require(['elements.base'], function() {
elation.elements.define('ui.counter', class extends elation.elements.base {
init() {
super.init();
this.defineAttributes({
value: { type: 'int', default: 0, set: this.updateDisplay },
step: { type: 'int', default: 1 },
});
}
create() {
this.addEventListener('click', () => this.value += this.step);
}
updateDisplay() {
this.innerHTML = this.value;
this.dispatchEvent({ type: 'change', data: this.value });
}
});
});Three hooks, called in order:
init()— declare attributes and set per-instance state. Callsuper.init()first.create()— fires once after the element is connected to the DOM and its children have parsed. Use this to wire event listeners, query children, and produce initial content.render()— optional; a hook for redrawing. Callthis.refresh()to trigger it.
defineAttributes({ name: descriptor }) bridges HTML attributes ↔ JS properties automatically. Descriptor keys:
type— the attribute's value type. Controls how it's coerced between its string form on the attribute and its typed form on the property. See Type system below for built-in types and how to register new ones.default— value used when the attribute is absent.get/set— hooks that fire on property read/write, handy for derived values or for reacting to changes.
Setting el.value = 42 from JS writes back to the HTML attribute, and el.setAttribute('value', '42') or <ui-counter value="42"> populates the typed property.
Extend elation.elements.base for standalone elements, or an existing element class (elation.elements.ui.list, elation.elements.ui.button, elation.elements.collection.simple, …) to build on its behavior. See the sidebar for available base classes.
HTML attributes are always strings; JS properties aren't. Elation's type system bridges the two: declare a type on an attribute and reads and writes coerce transparently. It's what makes <ui-counter value="42"> produce an element whose .value is the number 42, not the string "42" — and assigning el.value = 43 from JS writes value="43" back to the markup in the same shape.
The same declaration drives the set: and get: hooks from the previous section, which fire whenever a property is read or written. set: is how an element reacts to its own attribute changes without writing a separate attributeChangedCallback.
The four numeric / function / boolean types coerce in the type-system switch with no external dependencies:
boolean(alias:bool) — presence is true; an absent attribute,'0', or'false'is false. Writingtruesets the attribute to the empty string; writingfalseremoves it.integer(alias:int) — parsed withvalue|0, i.e. a 32-bit truncated integer.float(alias:number) — parsed with+value; non-numeric strings becomeNaN.callback— function values pass through; string values are wrapped innew Function('event', value), so inline handlers likeonaccept="this.submit()"work from markup the same wayonclick=does on native elements.
One additional type ships pre-registered via registerType() (covered below):
anchor— hybrid boolean / pixel-offset value used byui.panel's edge-snap attributes (top/bottom/left/right). Presence with no value or any non-numeric truthy value reads astrue; numeric values read as the offset. Used internally byui.windowso the same attribute can mean either "snap to top edge" (<ui-window top>) or "snap with a 50px offset" (<ui-window top="50">).
Unrecognized type names (including string, object, array) pass through with no coercion. That's harmless for string — attribute values are strings already — but for object and array it means markup attributes don't parse; only direct JS property assignment round-trips correctly. Declaring those types is still useful as a documentation hint and so editors and the docs generator can display the intent.
Anything richer — vectors, colors, URLs, dates — gets registered via elation.elements.registerType(name, handler). The handler is a { read, write } pair: read turns the raw attribute string into the typed value, and write turns the typed value back into a string for setAttribute. Core itself uses this to register anchor; consumers register types that depend on libraries core doesn't ship.
// Anchor is registered in core, alongside ui.panel — the hybrid type
// used for edge-snap attributes. Returns true for presence with no value,
// or a number when an explicit pixel offset is supplied.
elation.elements.registerType('anchor', {
read(value) {
if (value === true || value === '' || value === 'true') return true;
if (value === false || value == null || value === 'false') return false;
const n = Number(value);
if (isNaN(n) || n === 0) return true;
return n | 0;
},
write(value) {
if (value === true) return '';
if (value === false || value == null) return 'false';
return String(value);
}
});
// And in a project that ships Three.js (e.g., Elation Engine), a
// downstream registration that depends on a runtime core won't pull in:
elation.elements.registerType('vector3', {
read(value) {
if (value instanceof THREE.Vector3) return value;
const [x, y, z] = ('' + value).split(/\s+/).map(Number);
return new THREE.Vector3(x, y, z);
},
write(value) {
return `${value.x} ${value.y} ${value.z}`;
}
});With vector3 registered, any element can declare position: { type: 'vector3' } and its .position property is a live THREE.Vector3 while the markup stays valid as <my-element position="1 2 3">. Elation core registers types it can implement without external dependencies (like anchor); richer types are the consuming project's responsibility.
Collections are the data layer of the library — a consistent interface for list-shaped data that changes over time. Each collection holds an array of items and emits collection_add, collection_remove, collection_move, and collection_clear events when its contents change. List-style UI elements (ui.list, ui.grid, ui.tabs, ui.checklist, …) accept a collection reference and re-render incrementally as those events fire — no manual subscriptions, no full re-renders.
Set the collection property on the list element to a collection instance:
const users = elation.elements.create('collection-jsonapi', {
host: 'https://api.example.com',
endpoint: '/users',
});
const list = elation.elements.create('ui-list', {
collection: users,
append: document.body,
});
// list populates when users loads, and updates whenever
// users.add() / .remove() / .move() fires.For one-off load completion, listen on the collection directly:
elation.events.add(users, 'collection_load', () => {
console.log(`fetched ${users.items.length} users`);
});The class family covers three patterns of data backing, all built on the same event interface so any of them can drive any list element:
- In-memory —
simple(the base class; an ordered array with the four mutation events). - Remote —
api(REST),jsonapi(parses JSON responses),jsonpapi(script-tag callback for cross-origin endpoints that don't ship CORS headers). - Persistent + indexed —
indexed(uniqueness enforced by a per-item key),localindexed(auto-saves tolocalStorageand listens for cross-tab updates),sqlite(Node.js sqlite3 backing, server-side only).
Plus three derivers that expose a transformed view of another collection:
filter— apply a predicate; the derived collection contains only matching items.subset— re-shape a parent's raw response, so one API call can back several distinct list views.custom— supply anitemcallbackthat returns the items array on demand; useful for surfacing a non-array data structure (an object's keys, aMap, a computed view) as a collection.
See the collection namespace in the API docs for the full per-class attribute and event reference.
MIT — see LICENSE.