Build reactive UIs in the browser
with no build step.
Signal-based reactivity that runs natively with ESM/import maps. No virtual DOM. No compiler required — but an optional build-time compiler gives up to -44% faster renders. Use DOM-first JS libraries directly (Chart.js, Leaflet, AG Grid) and pick your path: web with Elur-UI or mobile with Elur-Ionic. Elur Query works in both.
Build full-stack apps with Elur Kit
File-based routing, SSG, SSR, ISR, islands, content collections, and zero client JS by default. Next.js conventions with Astro-style islands.
Explore Elur Kit -> Web PathBuild web products with Elur + Elur-UI
Compose interfaces with Elur-UI, and plug Elur Query when you need server-state, retries, and cache.
Launch web apps -> Mobile PathShip mobile with Elur-Ionic
Ionic routing + Elur reactivity for one codebase targeting web, Android, and iOS, with optional Elur Query data layer.
Launch mobile apps ->Why trust this approach?
Elur is not reinventing UI from scratch. It combines proven ideas from frameworks developers already trust: tagged templates, fine-grained signals, provide/inject, function components, and auto-tracking.
Use DOM-first libraries
without wrappers.
Real integration pattern: use refs, lifecycle hooks, and cleanup exactly as you would in production code.
Bring your existing stack as-is
Teams often reject frameworks when integration with existing JS libraries is painful. Elur keeps the native DOM model, so you can plug in charting, maps, grids, editors, and media players directly.
import { ElurComponent, html, signal, effect, ref } from "@elurjs/core"; import { Chart } from "chart.js/auto"; class SalesChart extends ElurComponent { private canvasRef = ref<HTMLCanvasElement>(); private points = signal([12, 19, 7]); private chart = null; render() { return html`<canvas ref=${this.canvasRef}></canvas>`; } onMount() { const ctx = this.canvasRef.el?.getContext("2d"); if (!ctx) return; this.chart = new Chart(ctx, { type: "line", data: { datasets: [{ data: this.points.value }] } }); effect(() => { this.chart.data.datasets[0].data = this.points.value; this.chart.update(); }); return () => this.chart?.destroy(); } }
From zero to reactive
in three steps.
No compiler, no config files, no boilerplate. Just install, write, and go.
Add to your project
One package, zero runtime dependencies. Works with Vite, Webpack, or directly via ESM CDN.
$ npm install @elurjs/core
# or scaffold a full project
$ npx create-elur-app my-app
# or via ESM CDN (no install)
import { signal } from
"https://esm.sh/@elurjs/core@3.5.0";
Write your component
A plain function returning html``
is all you need. No class, no decorator, no JSX transform.
function App() {
const count = signal(0);
return html`
<p>${() => count.value}</p>
<button @click=${() => count.value++}>
Click me
</button>
`;
}
Render to the DOM
Call mount()
once. Every signal update after this happens automatically — no re-render calls, no manual DOM updates.
// index.html: <div id="app"></div>
mount(App(), "#app");
// That's it. The app is live. ✓
tsconfig.json,
no vite.config.ts,
no babel.config.js
required — run it straight from the browser with an import map.
Traditional Frameworks
- Configure build tool (Vite/Webpack)
- Set up Babel/SWC transpilation
- Bundle compilation (Heavy artifacts)
- Debug compiled/minified code
The Elur Way
- Create
index.html - Import via CDN or local ESM
- Write pure JS templates
- Debug exactly what you wrote
Everything you need,
nothing you don't.
A complete UI framework that fits in a single import. No virtual DOM overhead, no compiler step, no configuration files.
Fine-Grained Reactivity
Signals update only the exact DOM nodes that depend on changed data. No diffing, no reconciliation, no wasted renders.
No Compiler Required
Templates are standard JavaScript tagged template literals. No JSX transform, no SFC compiler, no
build-time magic needed. An optional build-time compiler (@elurjs/core-compiler) is available for up to -44% faster renders when you want maximum performance.
Batteries Included
Router, forms, stores, dependency injection, portals, error boundaries, transitions — all built-in. One import, zero config.
TypeScript Native
Every API is fully typed from the ground up. Typed injection keys, typed store signals, typed route params — real type safety.
Familiar Patterns
If you know Vue's provide/inject, React's hooks, or Solid's signals — you'll feel right at home. The best ideas, unified.
XSS Hardened
Interpolated values are inserted as text nodes, never parsed as HTML. URL attributes like
href/src are sanitized — javascript:, data:text/html and
other dangerous schemes are blocked automatically.
See the reactivity
in action.
These demos simulate how Elur signals, computed values, and effects work. Interact with them to see fine-grained reactivity.
const doubled = computed(
() => count.value * 2
);
const label = computed(
() => count.value === 0
? "zero"
: count.value > 0 ? "positive" : "negative"
);
const remaining = computed(
() => todos.value
.filter(t => !t.done.value).length
);
html`<ul>${() =>
repeat(todos.value, ...)
</ul>`;
time = signal("");
onMount() {
const id = setInterval(() => {
this.time.value = new Date()
.toLocaleTimeString();
}, 1000);
return () => clearInterval(id);
}
}
One change. One update.
Zero overhead.
Under the hood, Elur is a four-layer stack. Each layer does exactly one job — signal, compute, bind, render.
Reading a signal inside effect() or html`` automatically registers a
subscription. No .subscribe() calls, no decorator, no annotation needed.
Each reactive expression inside html`` compiles to exactly one effect(). When the
signal changes, that one effect updates that one text node or attribute — nothing else.
Before each re-run, an effect disposes its previous subscriptions and runs its cleanup function (if any). Unmounting a component tears down every effect it owns.
Setting a signal to the same value it already holds is a no-op. No downstream effects are triggered, no DOM work happens — not even a microtask.
Multiple signal writes inside batch() queue their effects until the batch ends. All
subscribers see a consistent snapshot, and the DOM updates exactly once.
Read a signal with untrack() to get its value without creating a subscription. Useful for
reading config or context inside an effect you don't want to re-trigger.
Write less, do more.
Clean, readable code that does exactly what you'd expect. No magic, no surprises.
Signals that just work.
Create reactive values with signal(),
derive with computed(),
and watch with effect().
Three primitives power the entire framework.
- ✓ Automatic dependency tracking
- ✓ Object.is equality — no wasted updates
- ✓ Batch multiple writes into one flush
- ✓ Self-cleaning effects with auto-disposal
- ✓ untrack() for reading without subscribing
import { signal, computed, effect } from "@elurjs/core"; // Reactive state const count = signal(0); const doubled = computed(() => count.value * 2); // Auto-runs when count changes effect(() => { console.log(`Count: ${count.value}`); console.log(`Doubled: ${doubled.value}`); }); count.value = 5; // logs: Count: 5, Doubled: 10 // Batch multiple writes — effect runs once batch(() => { count.value = 10; count.update(n => n + 1); });
Two styles. Your choice.
Function components for pages and display. Class components when you need lifecycle hooks. Both work seamlessly together.
- ✓ Function components — zero boilerplate
- ✓ Class components — lifecycle hooks
- ✓ Children & named slots
- ✓ DOM refs with ref()
- ✓ Auto-cleanup on unmount
// Function component — simple & clean function Counter(): ElurTemplate { const count = signal(0); return html` <p>${() => count.value}</p> <button @click=${() => count.value++}> +1 </button> `; } // Class component — with lifecycle class Clock extends ElurComponent { time = signal(new Date().toLocaleTimeString()); onMount() { const id = setInterval(() => { this.time.value = new Date() .toLocaleTimeString(); }, 1000); return () => clearInterval(id); } render() { return html`<span>${() => this.time.value}</span>`; } }
Client-side routing, built in.
No extra package. Switch between history or hash mode, attach typed route meta, restore scroll automatically, and keep dynamic params, guards, and lazy loading.
- ✓ History + hash routing modes
- ✓ Route meta available through resolve()
- ✓ Custom scrollBehavior restoration
- ✓ Nested routes with RouterView depth
- ✓ Navigation guards and lazy loading
import { createRouter, RouterView, Link, lazy } from "@elurjs/core"; const router = createRouter([ { path: "/", component: () => HomePage() }, { path: "/about", component: () => AboutPage() }, { path: "/dashboard", component: () => new DashboardLayout(), meta: { requiresAuth: true }, children: [ { path: "/stats", component: lazy( () => import("./pages/Stats")) }, { path: "/settings", component: lazy( () => import("./pages/Settings")) }, ], }, { path: "*", component: () => NotFound() }, ], { mode: "hash", scrollBehavior: (_to, _from, saved) => saved ?? { left: 0, top: 0 } }); // Auth guard using route meta from resolve() router.beforeEach((to) => { const match = router.resolve(to); if (match?.meta?.requiresAuth && !isAuth()) return "/login"; });
Global state in 5 lines.
Every property becomes a signal automatically. Add typed actions and derived getters, subscribe globally to changes, and reset with $reset().
- ✓ Auto-signalized properties
- ✓ Typed actions with full inference
- ✓ Optional gettersFactory for derived signals
- ✓ Global $subscribe(key, next, prev)
- ✓ $reset() to restore initial state
- ✓ Works in any component or module
import { createStore, computed } from "@elurjs/core"; const cart = createStore( { items: [] as string[], total: 0, }, (s) => ({ add(item: string) { s.items.update(arr => [...arr, item]); s.total.update(n => n + 1); }, remove(item: string) { s.items.update(arr => arr.filter(i => i !== item)); s.total.update(n => n - 1); }, clear() { cart.$reset(); }, }), (s) => ({ itemCount: computed(() => s.items.value.length), hasItems: computed(() => s.items.value.length > 0), }) ); cart.$subscribe((key, next, prev) => { console.log("Store change:", key, prev, "→", next); }); cart.add("Milk"); cart.itemCount.value; // 1 cart.hasItems.value; // true
Typed Forms, Dot Paths & Cross-Field Rules.
Manage complex forms with nested objects, cross-field rules, and dynamic arrays. Validation is fully typed and works with built-ins, custom validators, or schemas.
- ✓ Typed field validation (Zod/Valibot)
- ✓ Dot-path validators for nested fields
- ✓ Cross-field validators with allValues
- ✓ elurFieldArray for dynamic lists
- ✓ validateOn: 'blur' | 'input' | 'submit'
- ✓ isSubmitting & submitCount tracking
import { createForm, elurFieldArray, required, email, minLength } from "@elurjs/core"; const form = createForm({ profile: { email: "" }, password: "", confirmPassword: "" }, { validateOn: 'blur', validators: { "profile.email": [required(), email()], password: [required(), minLength(8)], confirmPassword: [ required(), (value, allValues) => value !== allValues?.password ? "Passwords do not match" : null ] } }); // Dynamic field array const { fields, append, remove } = elurFieldArray( [{ value: "" }], { value: [required(), email()] } ); const onSubmit = form.handleSubmit(values => { console.log("Form submit:", values, fields.value.length); });
One core.
Official packages.
Build with a minimal reactive core and scale with first-party tools like Elur Query, Elur Ionic, and Elur UI without dependency roulette.
Form Management
Built-in field validation, dynamic arrays, and Zod/Valibot interop. Now includes elurFieldArray
for dynamic lists.
const form = createForm(
{ name: "", email: "" },
{ validators: {
name: [required(), minLength(2)],
email: [required(), email()],
}}
);
Portals
Render modals, tooltips, and toasts outside the component tree. Supports outlet tokens, refs, and provide/inject.
const modal = portal(
html`<div class="modal">
<h2>Confirm action</h2>
<button @click=${close}>OK</button>
</div>`
);
Error Boundaries
Catch render and reactive errors gracefully. Show fallback UIs without crashing the entire application.
createErrorBoundary(
new DataWidget(),
(err) => html`
<p class="error">
Failed: ${String(err)}
</p>`
);
Transitions
CSS class-based enter/leave animations. No wrapper elements, JS hooks for full control, appear on first render.
transition(
() => show.value
? html`<p>Hello!</p>`
: null,
{ name: "fade", appear: true }
);
Async & Suspense
suspend() for async views, lazy() for code-splitting, and Elur Query for robust async requests, retries, and query cache invalidation.
suspend(
() => fetch("/api/users").then(r => r.json()),
(users) => html`
<ul>${users.map(u =>
html\`<li>${u.name}</li>\`
)}</ul>`,
{ invalidate: refreshKey }
);
Dependency Injection
Vue-style provide/inject with typed keys. Pass data down the tree without prop drilling. Nearest ancestor wins.
const THEME = createInjectionKey<
Signal<string>
>("theme");
provide(THEME, signal("dark"));
const theme = inject(THEME);
Everything you need
to build, test, and ship.
First-party tooling that fits the framework — a build-time compiler, editor extensions with LSP, a test harness, a Vite plugin, and two CLIs for scaffolding and code generation.
Elur Testing
NewRender components, interact with signals, and assert against the real DOM with helpers designed for Elur. No synthetic wrappers required.
const { getByText } = render(Counter());
expect(getByText("0")).toBeTruthy();
count.value++;
await waitFor(() =>
expect(getByText("1")).toBeTruthy()
);
Read docs →
Vite Plugin for Elur
NewDrop the plugin into your Vite config and get optimized Elur handling, better HMR, and template-aware transforms out of the box. Includes the optional build-time compiler for up to -44% faster renders.
import elur from "@elurjs/vite-plugin-elur";
export default defineConfig({
plugins: [elur()],
});
Read docs →
Build-Time Compiler
OptionalOptional compile-time compiler that parses html`` templates and generates direct DOM
manipulation code. Eliminates runtime TreeWalker and detectContext overhead. -28% average (-44% peak) faster renders, matches Solid on 6/9 CPU benchmarks.
import { compileTemplate } from "@elurjs/core-compiler";
// Used internally by the Vite plugin
// — no manual setup needed
Read docs →
VS Code Extension
NewSyntax highlighting for html`` templates and raw(), event binding autocomplete,
diagnostics, quick fixes, formatting, and snippets. Powered by a language server that also works in Neovim, Helix, Zed, and Emacs.
# Install from the VS Code Marketplace
$ code --install-extension elurjs.vscode-elur
# Or search "Elur" in the Extensions panel
Install →
Prettier Plugin
Format html`` tagged template literals with Prettier. Indentation, attribute wrapping, and
expression alignment that match the framework's style conventions.
$ npm install -D prettier-plugin-elur
// .prettierrc
{ "plugins": ["prettier-plugin-elur"] }
Read docs →
Language Server
The same LSP that powers the VS Code extension works in any editor with LSP support. Diagnostics, completion, hover, code actions, and formatting — all editor-agnostic.
# Neovim
:MasonInstall elur-language-server
# Helix / Zed / Emacs
# configure via LSP settings
Read docs →
create-elur-app CLI
NewScaffold a Elur project in seconds. Vanilla JS, TypeScript, or Ionic mobile templates with Vite, tests, and HMR pre-configured.
npx create-elur-app
my-app
cd my-app
npm run dev
Read docs →
Elur CLI
NewGenerate components, pages, stores, and services inside an existing project. Auto-detects Elur or Elur Ionic and picks the right template.
npx elur add component Button
npx elur dev
npx elur build
Read docs →
Next.js conventions.
Astro-style islands. Zero client
JS.
A full-stack meta-framework built on Elur signals — file-based routing, SSG, SSR, ISR, content collections, image optimization, middleware, and SPA-like navigation.
@elurjs/kit brings production framework features to Elur:
- ✓ File-based routing with dynamic routes,
catch-all, route groups,
generateStaticParams, and named layout slots (*.slot.ts). - ✓ Islands architecture — hydrate only
interactive components with
load,idle,visible, andonly(client-only) directives. Optionalfallback,ssr: false, andisSSR()for environment reads. - ✓ Zero client JS by default — pages ship as
static HTML unless you opt into hydration. Route-level code-splitting via per-island
import()chunks. - ✓ Content collections with typed Markdown, YAML frontmatter, and Zod validation.
- ✓ Server actions with
elurAction()— reactivepending,error, anddatasignals. Progressive enhancement via HTML form submissions.fail()andredirect()helpers. - ✓ Suspense streaming —
streamBoundary()emits<template>chunks that swap fallback content in-place when resolved.
getImage() API.
SHA-256 transform keys, atomic writes, bounded concurrency.
elur-kit adapter <name>.
404.page.ts and 500.page.ts rendered during SSG, SSR, and all adapters.
directive: "only", ssr: false),
fallback content, isSSR(), and hydration fix for islands without SSR DOM.
Config file renamed to elur.config.*. Happy-dom fully removed — SSR uses the core's
DOM-free renderToString directly.
import { html } from "@elurjs/core"; import type { PageProps } from "@elurjs/kit"; import { island } from "@elurjs/kit"; import { load } from "./page.data.ts"; import Counter from "../islands/Counter"; export default function HomePage({ data }: PageProps<typeof load>) { return html` <article> <h1>${data.title}</h1> <p>The answer is ${data.count}.</p> ${island("Counter", Counter, { initial: 0 }, "load")} </article> `; } // page.data.ts — server-side data loader export const load: PageDataLoad = async () => { return { title: "Hello Elur Kit", count: 42 }; }; // Zero client JS by default. // Islands hydrate only when you say so.
Elur Query goes beyond fetch + cache.
Queues,
offline mode, and command orchestration.
Built for real app workflows: command modes, retries, optimistic updates, and offline replay with a custom queue adapter.
@elurjs/query is CQRS-style state orchestration for Elur:
- ✓ createQuery for read operations with status/data/error signals.
- ✓ createCommand for mutations with retries, dedupe, invalidation, and optimistic rollback.
- ✓ Cache utilities like getQueryData, setQueryData, and updateQueryData.
keepPreviousData, dispose(),
and a robust stableStringify.
import { createCommand, CommandQueuedError, getQueryData, setQueryData } from "@elurjs/query"; const saveOrder = createCommand("orders/save", async (payload, { signal }) => { const res = await fetch("/api/orders", { method: "POST", body: JSON.stringify(payload), signal }); if (!res.ok) throw new Error("save failed"); return res.json(); }, { mode: "queueOffline", invalidate: ["orders/list"], retry: (count, err) => count < 3, retryDelay: (count) => Math.min(500 * 2 ** (count - 1), 5000), onMutate: (item) => { const prev = getQueryData("orders/list") ?? []; setQueryData("orders/list", [...prev, item]); return { prev }; }, onError: (_e, _item, ctx) => setQueryData("orders/list", ctx?.prev ?? []), offline: { adapter: myQueueAdapter, // implements CommandQueueAdapter isOnline: () => navigator.onLine, replayOnReconnect: true, maxReplayAttempts: 5 } } ); try { await saveOrder.executeAsync({ id: "A-100", total: 42 }); } catch (e) { if (e instanceof CommandQueuedError) { // queued offline; replay happens later } } await saveOrder.replayQueue();
Type-safe internationalization.
Reactive by
default.
The official i18n library for Elur. Built on signals, zero runtime dependencies, and designed for real-world apps.
@elurjs/i18n gives your Elur apps first-class internationalization:
- ✓ Type-safe keys and interpolation parameters with autocompletion.
- ✓ Reactive translations powered by Elur signals.
- ✓ Plugins for persistence, locale detection, router sync, head tags, forms, ICU pluralization, and dev overlay.
- ✓ Backends for inline messages, JSON files, and custom APIs.
import { createI18n } from "@elurjs/i18n"; import { headPlugin } from "@elurjs/i18n/plugins/head"; const i18n = createI18n({ locale: "es", fallbackLocale: "en", nestedFallback: true, messages: { es: { hello: "Hola {name}" }, en: { hello: "Hello {name}" } } }); headPlugin(i18n); i18n.t("hello", { name: "Deiver" }); // "Hola Deiver"
Authentication and authorization
built for Elur.
Driver-based auth with reactive signals. JWT, session cookies, API keys, and OIDC — all share the same policy engine and router guards.
@elurjs/auth is the official auth layer for the Elur ecosystem:
- ✓ createAuth with reactive session, user, token, and isAuthenticated signals.
- ✓ Drivers for JWT, session cookies, API keys, and OIDC with PKCE.
- ✓ Policy engine with RBAC, tenant-aware resolvers, and custom guards.
- ✓ Router integration via declarative
meta.authDSL and standalone guards.
import { createAuth, jwtDriver, rbacPolicy } from "@elurjs/auth"; const auth = createAuth({ driver: jwtDriver({ loginUrl: "/api/login" }), autoRefresh: true, identity: { roles: "roles", permissions: "permissions" }, }); auth.attachPolicy( rbacPolicy({ resolveRoles: (u, tenant) => tenant ? u.rolesByTenant[tenant] : u.roles, }), ); await auth.login({ email: "deiver@example.com", password: "secret" }); console.log(auth.isAuthenticated.value); // true console.log(auth.can("role:admin", { tenant: "acme" }).value);
Inspired by the best.
Refined into one.
Elur didn't emerge in a vacuum. It distills battle-tested ideas from the frameworks that shaped modern UI development — taking what works, discarding the overhead.
Lit pioneered the idea of using JavaScript's native tagged template literals to define HTML
templates — no compiler, no JSX, no virtual DOM. Elur adopts this exact approach: the html`` tag parses templates once and wires live
bindings directly to real DOM nodes.
import { html } from 'lit';
html`<p>Hello ${name}</p>`;
// Elur takes the same approach
import { html } from '@elurjs/core';
html`<p>${() => name.value}</p>`;
Solid.js proved that signal-based fine-grained reactivity doesn't need a virtual DOM — just
wire effects directly to DOM nodes. Elur adopts the same reactive core: signal(), computed(), and effect() are the three primitives that power
everything.
const [count, setCount] = createSignal(0);
createEffect(() => console.log(count()));
// Elur — same concept, unified API
const count = signal(0);
effect(() => console.log(count.value));
Vue 3's Composition API introduced provide/inject, watch(), and typed lifecycle hooks as first-class
citizens. Elur mirrors this exactly: typed injection keys via createInjectionKey(), watch() with immediate/once options, and onMount / onUnmount hooks.
provide('theme', ref('dark'));
const theme = inject('theme');
// Elur — typed keys
const THEME = createInjectionKey<Signal<string>>('theme');
provide(THEME, signal('dark'));
React proved that function components with colocated state are more composable than
class-only patterns. Elur supports both: function components (plain functions + html``, zero boilerplate) and class components
(ElurComponent) only when lifecycle hooks are
needed.
function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n+1)}>{n}</button>;
}
// Elur — no JSX, no compiler
function Counter(): ElurTemplate {
const n = signal(0);
return html`<button @click=${() => n.value++}>${() => n.value}</button>`;
}
Svelte's built-in transition: directive made
animations a first-class concern — without a separate animation library. Elur's transition() brings the same mental model: CSS
class-based enter/leave lifecycle with optional JS hooks and appear on first render.
<div transition:fade>Hello!</div>
// Elur — same idea, no compiler
transition(
() => show.value ? html`<p>Hello!</p>` : null,
{ name: 'fade', appear: true }
);
MobX introduced transparent reactive tracking — read a value inside a reaction, and you're
automatically subscribed, no boilerplate. S.js formalized this into a dependency graph with batch() and untrack(). Elur inherits both: effects
auto-track their dependencies and untrack()
lets you opt out selectively.
batch(() => {
price.value = 20; // writes queued
qty.value = 3; // effect runs once
});
effect(() => {
const a = price.value;
// not tracked
const b = untrack(() => qty.value);
});
The best frameworks aren't built from scratch — they're built on the shoulders of great ideas. Elur studies what works across the ecosystem and brings it together: tagged templates from Lit, fine-grained signals from Solid, provide/inject from Vue, function components from React, CSS transitions from Svelte, and transparent auto-tracking from MobX — unified into a single, zero-dependency, compiler-free package that respects your time and your bundle size.
Elur goes mobile.
Elur-Ionic bridges Elur reactivity with the full Ionic component library.
Build native-quality mobile apps with signals, client-side routing, and modular loading.
Since v2.0, the router uses the core Elur router (no ion-router), with IonRouterOutlet, createTabsLayout, and createBottomTabBar for tab navigation.
Ionic Web Components.
Elur Reactivity.
Install @elurjs/ionic
and let the Vite plugin auto-register only the <ion-*> tags you use — or import bundles manually for full control.
- ✓ Auto-registration via Vite plugin — zero manual setup
- ✓ Or import bundles manually: layout, forms, overlays, navigation…
- ✓ Core bootstrap registers only essential Ionic elements
- ✓ Routing via core Elur router +
IonRouterOutlet - ✓
createTabsLayout&createBottomTabBarfor tab apps - ✓ Compatible with Capacitor for true native deployment
- ✓ Signals-first — all state is reactive by default
import { ElurComponent, html, mount, elurRouter } from "@elurjs/core"; import { IonRouterOutlet } from "@elurjs/ionic"; // Auto-generated by the Vite plugin — scans html`` templates // for <ion-*> tags and registers only what you use. import "virtual:elur-ionic/registration"; import { HomePage } from "./pages/HomePage"; import { TaskDetailPage } from "./pages/TaskDetailPage"; const router = elurRouter(); // Elur router + IonRouterOutlet (no ion-router needed) const outlet = new IonRouterOutlet([ { path: "/", component: (ctx) => new HomePage(ctx) }, { path: "/task/:id", component: (ctx) => new TaskDetailPage(ctx) }, ]); class App extends ElurComponent { render() { return html`<ion-app>${outlet}</ion-app>`; } } mount(new App(), "#app");
Auto-Registration
The Vite plugin scans html`` templates for <ion-*> tags and generates
registration imports automatically. Zero manual setup — or import bundles manually for full control.
Tree-Shakeable Bundles
8 category bundles (layout, navigation, forms, lists, feedback, buttons, overlays, all) + per-component subpath imports. Minimal fixture = 11.3% of full bundle (gzip).
Reactive Overlays
Signal-based createToast(), createAlert(), createLoading(),
createActionSheet(), createPopover(), createModal(),
createPicker() — with presented and result signals, latest-wins
semantics, and stale-result protection.
Cache Policies
LRU/FIFO max eviction, TTL expiry, per-route overrides, and per-tab cache isolation. Keep pages alive across navigation or expire them automatically.
Core Router Integration
Single router authority — no competing Ionic/Elur routers. IonRouterOutlet +
IonBackButton + reactive canGoBack. Deep linking, tab stacks, and history-api
navigation work out of the box.
Tab System
createTabsLayout() wraps outlet + tab bar in <ion-tabs> with correct CSS
layout. createBottomTabBar() with badges, CSS vars, layout options, and Stencil
selected sync via ref + effect.
Page-State Persistence
Opt-in serializable state across navigation. Preserve form inputs, scroll position, and component state when users navigate back and forth.
Optional Capacitor
@elurjs/ionic/capacitor subpath with zero web bundle cost (604 bytes). StatusBar,
SplashScreen, Keyboard, Haptics, and App plugin wrappers with graceful web degradation (no-op on web).
Framework Delegate
Mount Elur templates and components inside Ionic overlays (modal, popover) via a Elur
FrameworkDelegate — uses mount() to render and unmount() on
dismiss.
Tested & Verified
236 unit tests + 56 E2E tests with real @ionic/core (no mocks): navigation, lifecycle,
overlays, custom elements, accessibility, and leak detection.
Available Bundles — v2.0.7
Tree-shakeable- import { elurIonicRouter } from "@elurjs/ionic";
- const r = elurIonicRouter();
+ import { elurRouter } from "@elurjs/core";
+ import { IonRouterOutlet, createTabsLayout } from "@elurjs/ionic";
+ const r = elurRouter();
+ const outlet = new IonRouterOutlet(routes, { tabs: ["/home", "/profile"] });
// Overlays — use* renamed to create*
- const toast = useToast();
+ import { createToast } from "@elurjs/ionic";
+ const toast = createToast();
// Components — auto-registration via Vite plugin
- setupElurIonic(); // registered everything
+ import "virtual:elur-ionic/registration"; // auto from templates
BikerOS: the OS for motorcycle clubs
A full-stack platform for motorcycle clubs — live GPS tracking, SOS emergencies, route management, events, and a web admin panel. Built entirely on the Elur ecosystem across 4 apps.
Built on the Elur ecosystem
Elur-Ionic + Elur Query. SOS emergencies, live GPS tracking, offline routes, ICE medical info, event RSVP.
Elur Kit with SSG, islands, SEO, sitemap, and content collections. Pricing, features, FAQ.
Elur + Elur Query. Member management, route builder with map, event dashboard, billing and invoicing.
BikerOS white-label instance for the Iron Bikers club. Custom branding, domain, and club-specific features.
Built with Elur
Real production apps powered by the Elur ecosystem — from mobile to web to full-stack platforms.
BikerOS Landing
Marketing site for a motorcycle club platform. SSG with islands for interactive pricing, FAQ accordion, and scroll reveal. SEO, sitemap, and content collections out of the box.
Visit bikeros.co
BikerOS Mobile App
Motorcycle club management app with SOS emergencies, live GPS tracking, offline routes, ICE medical info, and event RSVP. Elur-Ionic components, Elur Query for server state, and Capacitor for native deployment.
Android & iOS
BikerOS Admin Panel
Web dashboard for club administrators. Member management, route builder with interactive maps, event dashboard, billing and invoicing. Elur for UI, Elur Query for data fetching and cache.
Web app
Elur Kit Docs
The official Elur Kit documentation — built with Elur Kit itself. File-based routing, SSG, islands, Shiki code highlighting, full-text search, and SEO out of the box.
Visit project
University Academic System
An academic tracking platform showcasing client-side routing, global state management, and nested layouts powered by Elur.
Visit projectCommon questions,
straight answers.
@elurjs/core-compiler) that
generates direct DOM manipulation code, eliminating runtime TreeWalker and detectContext overhead
(-28% average, up to -44% faster renders). The Vite plugin (@elurjs/vite-plugin-elur >= 2.0.2) uses
the compiler automatically — no manual setup needed.
Elur Kit 2.4.4 adds client-only islands (directive: "only", ssr: false),
fallback content, isSSR() for environment reads, and fixes hydration
for islands without SSR DOM.
The ecosystem packages @elurjs/query 1.5.1, @elurjs/ionic 2.0.7,
@elurjs/auth 1.2.2, and @elurjs/i18n 1.3.2 are aligned.
@elurjs/core-compiler)
is optional and only improves performance by -28% on average (up to -44%) on rendering benchmarks.
The Vite plugin enables it automatically when installed, but you can use Elur without any
bundler at all.
Chart.js, Leaflet,
and AG Grid without wrappers. If it runs in browser JavaScript, you can integrate it.
@elurjs/query for async requests, query cache, retries, and invalidation.
It is platform-agnostic and works in both web and mobile stacks. Start with:
npm install @elurjs/core @elurjs/query.
@elurjs/ionic@2.0.7 with Ionic Core for routing + native-style UI, then wrap
with Capacitor for Android/iOS deployment using the same codebase.
Three paths.
Kit, web, or mobile.
Build full-stack apps with Elur Kit, web apps with Elur + Elur-UI, or ship mobile apps with Elur-Ionic. Add Elur Query as the same async/cache layer in any path.
Help us build the next generation of reactive UIs.
Elur is an open-source project built by developers, for developers. Whether it's a bug report, a feature request, or a pull request, your contribution matters.