New: Elur Kit 2.4.4 Client-only islands, optional build-time compiler, and zero client JS by default. Explore →
v3.5.0 — Optional build-time compiler, client-only islands, leaner core
Elur Kit 2.4.4 — Full-stack meta-framework →

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.

TypeScript-first MIT License 1054 tests passing 92.9% Coverage ~15 KB gzipped Elur UI Elur Query 1.5.1 Elur Auth 1.2.2 Elur Kit 2.4.4 VS Code Extension Zero dependencies
~15 KB
Gzipped bundle
-90%
Best-case JS-only gain
1054
Tests passing
100%
TypeScript typed

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.

Lit Templates like Lit, without Web Components overhead.
Solid.js Fine-grained reactivity for pure ESM projects.
Vue 3 The provide/inject pattern you love, zero-dep.
React Functional components without hook-rule headaches.
Svelte Reactive DX without needing a .svelte compiler.
MobX Transparent state tracking for any JS object.
View technical inspiration breakdown

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.

Chart.js Visualizations
MapLibre Dynamic Maps
AG Grid Enterprise Grids
Any JS native DOM plugin
chart-integration.ts
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.

Install

Add to your project

One package, zero runtime dependencies. Works with Vite, Webpack, or directly via ESM CDN.

# npm
$ 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";
Create

Write your component

A plain function returning html`` is all you need. No class, no decorator, no JSX transform.

import { signal, html } from "@elurjs/core";

function App() {
  const count = signal(0);
  return html`
    <p>${() => count.value}</p>
    <button @click=${() => count.value++}>
      Click me
    </button>
  `
;
}
Mount

Render to the DOM

Call mount() once. Every signal update after this happens automatically — no re-render calls, no manual DOM updates.

import { mount } from "@elurjs/core";

// index.html: <div id="app"></div>

mount(App(), "#app");

// That's it. The app is live. ✓
No 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.

signal() + computed() Live
const count = signal(0);
const doubled = computed(
  () => count.value * 2
);
const label = computed(
  () => count.value === 0
    ? "zero"
    : count.value > 0 ? "positive" : "negative"
);
📋 repeat() + signal() Native ESM Live
const todos = signal([]);
const remaining = computed(
  () => todos.value
    .filter(t => !t.done.value).length
);

html`<ul>${() =>
  repeat(todos.value, ...)
</ul>`;
🕐 effect() + lifecycle Live
class Clock extends ElurComponent {
  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.

signal()
Holds a reactive value. Notifies subscribers on write.
🔗
computed()
Derives a value. Re-runs only when its dependencies change.
🔄
effect()
Auto-tracks reads. Re-runs when any tracked signal changes.
🧩
html``
Parses once. Each binding is one effect targeting one DOM node.
🖥️
DOM Node
Only the exact node that changed gets updated. No diffing.
Subscription is automatic

Reading a signal inside effect() or html`` automatically registers a subscription. No .subscribe() calls, no decorator, no annotation needed.

🔁 Effect = DOM binding

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.

🧹 Self-cleaning effects

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.

🎯 Object.is equality

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.

📦 batch() flushes once

Multiple signal writes inside batch() queue their effects until the batch ends. All subscribers see a consistent snapshot, and the DOM updates exactly once.

🔒 untrack() for reads

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
counter.ts
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
components.ts
// 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
router.ts
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
store.ts
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
forms.ts
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

New

Render 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

New

Drop 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

Optional

Optional 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

New

Syntax 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

New

Scaffold 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

New

Generate 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:

SSG SSR ISR Islands API Routes Server Actions Streaming Adapters
  • 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, and only (client-only) directives. Optional fallback, ssr: false, and isSSR() 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() — reactive pending, error, and data signals. Progressive enhancement via HTML form submissions. fail() and redirect() helpers.
  • Suspense streamingstreamBoundary() emits <template> chunks that swap fallback content in-place when resolved.
SSG + SSR + ISR Static generation, server rendering, and incremental regeneration per route.
Image Optimization Build-time WebP/AVIF with srcset/sizes, lazy loading, and getImage() API. SHA-256 transform keys, atomic writes, bounded concurrency.
Middleware Auth, redirects, and header injection before routing.
SPA router Client-side navigation with style hoisting and no flash.
Cache Adapters Filesystem, Redis, Upstash, and Cloudflare KV. Tag-based invalidation.
Deployment Adapters Vercel, Netlify, Bun, and Node. One command: elur-kit adapter <name>.
Custom Error Pages 404.page.ts and 500.page.ts rendered during SSG, SSR, and all adapters.
Security Hardened CSRF origin checks, path traversal fuzz tests, production error sanitization.
v2.4.4 — client-only islands (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.
Explore Elur Kit
src/app/page.ts
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:

latest queue parallel queueOffline
  • 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.
Offline Queue + Replay Queue commands while offline and replay on reconnect.
CommandQueuedError Distinguish queued-offline from real command failures.
Optimistic Rollback Use onMutate/onError to keep UI fast and safe.
Retry + Backoff Fine-grained retry policy per command.
v1.5.1 adds single-flight request dedup, keepPreviousData, dispose(), and a robust stableStringify.
View Elur Query on npm
orders-command.ts
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.
v1.3.2 fixes 8 critical issues: no caching of failed requests, AST-based CLI extraction, and a plugin composition pipeline.
View docs on GitHub
i18n.ts
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.auth DSL and standalone guards.
v1.2.2 adds transient error retry in refresh(), multi-tab sync via BroadcastChannel, and a reactive useAuth().
View Elur Auth on npm
auth.ts
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
google.github.io/lit
Tagged Templates

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.

// Lit's html tag (the original idea)
import { html } from 'lit';
html`<p>Hello ${name}</p>`;

// Elur takes the same approach
import { html } from '@elurjs/core';
html`<p>${() => name.value}</p>`;
lit.dev
Solid.js
solidjs.com
Fine-Grained Signals

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.

// Solid.js reactive primitives
const [count, setCount] = createSignal(0);
createEffect(() => console.log(count()));

// Elur — same concept, unified API
const count = signal(0);
effect(() => console.log(count.value));
solidjs.com
Vue 3
vuejs.org
Composition API

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.

// Vue 3 — provide / inject
provide('theme', ref('dark'));
const theme = inject('theme');

// Elur — typed keys
const THEME = createInjectionKey<Signal<string>>('theme');
provide(THEME, signal('dark'));
vuejs.org
React
react.dev
Hooks Model

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.

// React — function component + state
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>`;
}
react.dev
Svelte
svelte.dev
CSS Transitions

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.

// Svelte — transition directive
<div transition:fade>Hello!</div>

// Elur — same idea, no compiler
transition(
  () => show.value ? html`<p>Hello!</p>` : null,
  { name: 'fade', appear: true }
);
svelte.dev
MobX & S.js
Observer pattern
Auto-tracking

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.

// Elur batch + untrack — from MobX/S.js
batch(() => {
  price.value = 20;  // writes queued
  qty.value = 3;    // effect runs once
});

effect(() => {
  const a = price.value;     
// not tracked
  const b = untrack(() => qty.value);
});
mobx.js.org

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.

— Design philosophy of Elur

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 & createBottomTabBar for tab apps
  • Compatible with Capacitor for true native deployment
  • Signals-first — all state is reactive by default
$ npm install @elurjs/ionic
Elur-Ionic 2.0.7 Elur 3.5.0 compatible Ionic Core 8.x
main.ts
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
layout
header, toolbar, title, content, footer, buttons
navigation
menu, menu-button, menu-toggle
forms
input, textarea, checkbox, radio, select, toggle, searchbar
lists
list, item, label, thumbnail, avatar, card
feedback
spinner, progress-bar, skeleton-text, badge, chip, note
buttons
button, fab, fab-button, fab-list, ripple-effect
overlays
modal, popover, toast, alert, action-sheet, picker
all
Everything — backward-compatible, ~35 KB gz
Migration guide (v1.x -> v2.x — major changes)
// Router — no more ion-router, single authority
- 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
Production Case Study

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.

Elur-Ionic (Mobile) Elur Kit (Landing + Web) Elur Query (Data) SSG + SSR Offline-first White-label ready
2 Apps shipped
6 SOS types
40+ Members per club
100% Elur ecosystem
BikerOS App — SOS Emergency BikerOS App — Home Dashboard BikerOS App — Events & Rides

Built on the Elur ecosystem

Mobile App
BikerOS App

Elur-Ionic + Elur Query. SOS emergencies, live GPS tracking, offline routes, ICE medical info, event RSVP.

Landing Page
bikeros.co

Elur Kit with SSG, islands, SEO, sitemap, and content collections. Pricing, features, FAQ.

Admin Panel
BikerOS Web

Elur + Elur Query. Member management, route builder with map, event dashboard, billing and invoicing.

White-label
Iron Bikers

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 page — built with Elur Kit (SSG, islands, SEO)
Elur Kit SSG + Islands

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 — built with Elur-Ionic and Elur Query
Elur-Ionic Mobile + Offline

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 — built with Elur and Elur Query
Elur + Query Dashboard

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 documentation site — built with Elur Kit itself (dogfooding)
Elur Kit Dogfooding

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
Academic Management System built using Elur and Elur-UI components
Elur SPA

University Academic System

An academic tracking platform showcasing client-side routing, global state management, and nested layouts powered by Elur.

Visit project

Common questions,
straight answers.

Still have questions?

Browse the full documentation or open an issue on GitHub. The community is small but growing fast.

Open an issue on GitHub
Elur 3.5.0 adds an optional build-time compiler (@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.
No. Elur works with zero build step — templates are standard JavaScript tagged template literals that run natively via ESM/import maps. The build-time compiler (@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.
Yes. It is designed for production with zero runtime dependencies in the core, strong TypeScript support, and a practical ecosystem for web and mobile workflows.
Yes. Elur works directly with DOM-first libraries like Chart.js, Leaflet, and AG Grid without wrappers. If it runs in browser JavaScript, you can integrate it.
Use @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.
Yes. Use @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.
Usually minimal. Most upgrades are dependency updates plus a quick validation pass over routes, reactive effects, and tooling integrations.

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.