{"id":49534316,"url":"https://github.com/waelbettayeb/elements-kit","last_synced_at":"2026-05-09T09:29:01.966Z","repository":{"id":331361523,"uuid":"1126331700","full_name":"waelbettayeb/elements-kit","owner":"waelbettayeb","description":"A set of universal reactive primitives for building web UI","archived":false,"fork":false,"pushed_at":"2026-04-29T13:04:34.000Z","size":1146,"stargazers_count":11,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T14:40:32.122Z","etag":null,"topics":["builder","custom-elements","frontend","html","js","library","reactivity","signals","typescript","ui","web","webcomponents"],"latest_commit_sha":null,"homepage":"https://elements-kit.quba.co","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/waelbettayeb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-01T17:18:35.000Z","updated_at":"2026-04-27T18:25:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/waelbettayeb/elements-kit","commit_stats":null,"previous_names":["waelbettayeb/adam","waelbettayeb/elements-kit"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/waelbettayeb/elements-kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waelbettayeb%2Felements-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waelbettayeb%2Felements-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waelbettayeb%2Felements-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waelbettayeb%2Felements-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/waelbettayeb","download_url":"https://codeload.github.com/waelbettayeb/elements-kit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waelbettayeb%2Felements-kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32528665,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T01:12:54.858Z","status":"online","status_checked_at":"2026-05-02T02:00:05.923Z","response_time":132,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["builder","custom-elements","frontend","html","js","library","reactivity","signals","typescript","ui","web","webcomponents"],"created_at":"2026-05-02T09:05:00.902Z","updated_at":"2026-05-09T09:29:01.959Z","avatar_url":"https://github.com/waelbettayeb.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ElementsKit 🌱\n\n**Universal reactive primitives for the web.** Signals, JSX, custom elements, and browser-API helpers. Import one at a time, compose them, or use any of them inside vanilla JS, React, Vue, or any framework.\n\n```tsx\nimport { signal, computed } from \"elements-kit/signals\";\nimport { render } from \"elements-kit/render\";\nimport type { ReactiveProps } from \"elements-kit/jsx-runtime\";\n\nfunction Counter(props: ReactiveProps\u003c{ initial?: number }\u003e) {\n  const count = signal(props.initial() ?? 0);\n  const doubled = computed(() =\u003e count() * 2);\n\n  return (\n    \u003csection\u003e\n      \u003cp\u003e\u003cstrong\u003e{count}\u003c/strong\u003e × 2 = \u003cstrong\u003e{doubled}\u003c/strong\u003e\u003c/p\u003e\n      \u003cbutton on:click={() =\u003e count(count() + 1)}\u003e+1\u003c/button\u003e{\" \"}\n      \u003cbutton on:click={() =\u003e count(count() - 1)}\u003e−1\u003c/button\u003e\n    \u003c/section\u003e\n  );\n}\n\nrender(document.getElementById(\"app\")!, () =\u003e \u003cCounter initial={0} /\u003e);\n```\n\n## Installation\n\n```sh\nnpm install elements-kit\n```\n\nConfigure JSX in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"elements-kit\"\n  }\n}\n```\n\n## Why ElementsKit\n\nElementsKit is a library of reactive primitives, not a framework. Each piece is its own import, runs on its own, and composes with the others — inside React, inside a custom element, or on its own in a script.\n\n- **Compose, don't configure.** Small focused APIs — `signal`, `computed`, `on`, `fromEvent`, `async`. Combine primitives instead of maintaining an overloaded interface.\n\n- **Close to the platform.** JSX compiles to `document.createElement`. `promise` extends `Promise`. Custom elements *are* `HTMLElement`. Thin or absent abstraction layers — no virtual DOM, no proxies, no build steps.\n\n- **Predictable and explicit — no magic.** `signal/compose` are reactive; nothing else is. No heuristic dependency tracking, no hidden subscriptions.\n\n- **Designed for the AI age.** Code is cheap; maintenance still isn't. Primitives compose into higher-level blocks. Swap one block at a time instead of maintaining long lines of code.\n\n- **Bundler-friendly.** Every primitive is its own subpath — `elements-kit/signals`, `elements-kit/utilities/*`, `elements-kit/integrations/*`. Import only what you need.\n\n## Packages\n\nEvery feature is a separate subpath export — import only what you use.\n\n| Entry | Purpose |\n|-------|---------|\n| `elements-kit/signals` | `signal`, `computed`, `effect`, `effectScope`, `batch`, `untracked`, `trigger`, `onCleanup`, `MaybeReactive`, `resolve`, `resolveProps`, `@reactive` |\n| `elements-kit/render` | `render(target, setup)` — mount a node with a scoped lifetime; returns `unmount` |\n| `elements-kit/attributes` | `@attributes` decorator + `ATTRIBUTES` symbol |\n| `elements-kit/slot` | `Slot`, `Slots`, `SLOTS` symbol — comment-marker DOM regions |\n| `elements-kit/custom-elements` | `defineElement`, `CustomElementRegistry` |\n| `elements-kit/for` | `For` keyed-list component |\n| `elements-kit/jsx-runtime` | JSX factory + type helpers (`ElementProps`, `Props`, `ComponentProps`, `MaybeReactiveProps`, `ReactiveProps`, `Require`) — configure via `jsxImportSource` |\n| `elements-kit/integrations/react` | `useSignal`, `useScope` React bridge hooks |\n| `elements-kit/utilities/*` | Reactive browser-API utilities — see [src/utilities/README.md](src/utilities/README.md) |\n\n## Signals\n\nFine-grained reactive state. Signals track their dependencies automatically — only the exact computeds and effects that depend on a changed signal are re-evaluated.\n\n```ts\nimport { signal, computed, effect, batch, untracked, onCleanup } from \"elements-kit/signals\";\n\nconst count = signal(0);\nconst doubled = computed(() =\u003e count() * 2);\n\nconst stop = effect(() =\u003e {\n  console.log(\"count:\", count()); // runs on every change\n});\n\ncount(1);  // → count: 1\ncount(2);  // → count: 2\nstop();    // unsubscribe\n\nbatch(() =\u003e { count(10); count(20); }); // single notification\n\nconst raw = untracked(() =\u003e count()); // read without subscribing\n\neffect(() =\u003e {\n  const id = setInterval(() =\u003e count(count() + 1), 1000);\n  onCleanup(() =\u003e clearInterval(id)); // runs before re-run or on stop\n});\n```\n\n### Store\n\nA **store** is a class whose fields are made reactive with `@reactive`. It holds shared state — no `render()`, no DOM — and any subscriber updates automatically.\n\n```ts\nimport { reactive, computed } from \"elements-kit/signals\";\n\nexport class CartStore {\n  @reactive() items: { name: string; price: number }[] = [];\n  @reactive() discount = 0;\n\n  total = computed(() =\u003e\n    this.items.reduce((s, i) =\u003e s + i.price, 0) * (1 - this.discount),\n  );\n\n  add(item: { name: string; price: number }) {\n    this.items = [...this.items, item];\n  }\n}\n\nexport const cart = new CartStore();\n```\n\nStores are **framework-agnostic** — the same instance drives a custom element, a React component, and a plain effect in sync.\n\n## JSX → DOM\n\nJSX compiles directly to `document.createElement`. No virtual DOM, no diffing.\n\n```tsx\n// This:\nconst el = \u003cbutton on:click={() =\u003e count(count() + 1)}\u003e{count}\u003c/button\u003e;\n\n// Is equivalent to:\nconst el = document.createElement(\"button\");\nel.addEventListener(\"click\", () =\u003e count(count() + 1));\n// `count` signal creates a live text node — updates in place on change\n```\n\nPassing a signal or `() =\u003e T` as a child or prop creates a **live binding** — the DOM updates in place, never re-rendering the surrounding tree.\n\n```tsx\nconst name = signal(\"Alice\");\n\n\u003cp\u003eHello, {name}!\u003c/p\u003e             // live text node\n\u003cinput value={name} /\u003e             // live attribute\n\u003cdiv class:active={computed(() =\u003e name() !== \"\")} /\u003e  // reactive class\n\u003cspan style:color={signal(\"red\")} /\u003e  // reactive style\n```\n\n### Prop namespaces\n\n| Syntax | Effect |\n|--------|--------|\n| `{signal}` / `{() =\u003e fn()}` | Live-bound reactive child |\n| `on:click={fn}` | Event listener (case-preserving event name) |\n| `class:active={bool}` | Reactive `classList.toggle` |\n| `style:color={value}` | Reactive inline style property |\n| `prop:foo={val}` | Force property assignment (skips `setAttribute`) |\n\n## Class Components\n\nAny class with a `render()` method returning an `Element` is a component. Components own their state and produce elements.\n\n```tsx\nimport { reactive, computed } from \"elements-kit/signals\";\nimport { render } from \"elements-kit/render\";\n\nclass Counter {\n  @reactive() count = 0;\n  doubled = computed(() =\u003e this.count * 2);\n\n  render() {\n    return (\n      \u003csection\u003e\n        \u003cp\u003e{() =\u003e this.count} × 2 = {this.doubled}\u003c/p\u003e\n        \u003cbutton on:click={() =\u003e this.count++}\u003e+1\u003c/button\u003e\n      \u003c/section\u003e\n    ) as Element;\n  }\n}\n\nconst unmount = render(document.getElementById(\"app\")!, () =\u003e \u003cCounter/\u003e);\n```\n\n## Custom Elements\n\nElementsKit enhances native `HTMLElement` subclasses — start with the platform, add only what you need.\n\n```tsx\nimport { reactive, computed } from \"elements-kit/signals\";\nimport { attributes, ATTRIBUTES as attr } from \"elements-kit/attributes\";\nimport { render } from \"elements-kit/render\";\n\n@attributes\nclass CounterElement extends HTMLElement {\n  static [attr] = {\n    count(this: CounterElement, value: string | null) {\n      this.count = Number(value ?? 0);\n    },\n  };\n\n  @reactive() count = 0;\n  doubled = computed(() =\u003e this.count * 2);\n\n  #unmount?: () =\u003e void;\n\n  connectedCallback() {\n    this.#unmount = render(this, () =\u003e (\n      \u003csection\u003e\n        \u003cp\u003e{() =\u003e this.count} × 2 = {this.doubled}\u003c/p\u003e\n        \u003cbutton on:click={() =\u003e this.count++}\u003e+1\u003c/button\u003e\n      \u003c/section\u003e\n    ));\n  }\n\n  disconnectedCallback() {\n    this.#unmount?.();\n    this.#unmount = undefined;\n  }\n}\n\ncustomElements.define(\"x-counter\", CounterElement);\n```\n\n`\u003cx-counter count=\"5\" /\u003e` — attribute bound, reactive, works in any HTML context.\n\n### Typed JSX for custom elements\n\nRegister the tag and augment the `CustomElementRegistry` interface — JSX infers the full prop shape (attributes, events, slots, children) from the class itself.\n\n```ts\nimport { defineElement } from \"elements-kit/custom-elements\";\n\ndefineElement(\"x-counter\", CounterElement);\n\ndeclare module \"elements-kit/custom-elements\" {\n  interface CustomElementRegistry {\n    \"x-counter\": typeof CounterElement;\n  }\n}\n\n// Now `\u003cx-counter count={5} /\u003e` is fully typed — no hand-written `declare global` block.\n```\n\nSee [Types](docs/src/content/docs/elements/types.mdx) for the full set of prop-inference helpers.\n\n## React Integration\n\nConnect signals and stores to React components via `useSyncExternalStore`:\n\n```tsx\nimport { useSignal, useScope } from \"elements-kit/integrations/react\";\nimport { cart } from \"./cart-store\";\n\nfunction CartSummary() {\n  // Reads a @reactive field — re-renders only when cart.items changes\n  const items = useSignal(() =\u003e cart.items);\n  const total = useSignal(cart.total); // Computed\u003cT\u003e works directly\n\n  // Effects tied to this component's lifetime\n  useScope(() =\u003e {\n    effect(() =\u003e console.log(\"cart updated:\", items));\n  });\n\n  return \u003cp\u003e{items.length} items — ${total.toFixed(2)}\u003c/p\u003e;\n}\n```\n\nThe same `cart` store drives custom elements, React trees, and plain scripts — all in sync.\n\n## Utilities\n\nPre-built reactive wrappers around common browser APIs. Each utility lives at its own subpath (`elements-kit/utilities/\u003cname\u003e`) and ships as its own entry — you pay only for what you import. Full catalog in [src/utilities/README.md](src/utilities/README.md).\n\n`createMediaQuery` wraps `window.matchMedia` into a reactive signal — reads inside effects or computeds re-run automatically when the media query result changes.\n\n```tsx\nimport { effect } from \"elements-kit/signals\";\nimport { createMediaQuery } from \"elements-kit/utilities/media-query\";\n\nconst isDark = createMediaQuery(\"(prefers-color-scheme: dark)\");\nconst isMobile = createMediaQuery(\"(max-width: 640px)\");\n\neffect(() =\u003e document.documentElement.classList.toggle(\"dark\", isDark()));\n```\n\nSingletons like `online`, `windowFocused`, `activeElement`, and `currentLocation` are pre-instantiated — import and read them directly inside any reactive context.\n\n```ts\nimport { effect } from \"elements-kit/signals\";\nimport { online } from \"elements-kit/utilities/network\";\nimport { windowFocused } from \"elements-kit/utilities/window-focus\";\n\neffect(() =\u003e console.log(\"online:\", online(), \"focused:\", windowFocused()));\n```\n\n## Async \u0026 Promise\n\nTwo primitives convert imperative async work into reactive state: `promise` (minimal, any `Promise` → reactive state) and `async` (full controller with start/stop/run and optional reactive input).\n\n### `promise`\n\nWraps an async function (or raw `Promise`) into a `ComputedPromise\u003cT\u003e` — awaitable **and** callable as a reactive value. Exposes `.state`, `.value`, `.reason`, `.result` as reactive reads.\n\n```ts\nimport { promise } from \"elements-kit/utilities/promise\";\nimport { effect } from \"elements-kit/signals\";\n\nconst user = promise(() =\u003e fetch(\"/api/user\").then((r) =\u003e r.json()));\n\neffect(() =\u003e {\n  if (user.state === \"pending\")   console.log(\"loading…\");\n  if (user.state === \"fulfilled\") console.log(\"user:\", user.value);\n  if (user.state === \"rejected\")  console.log(\"error:\", user.reason);\n});\n\nawait user; // awaitable\n```\n\n`ReactivePromise` is the underlying class — use it when you want the reactive state getters without the `Computed` callable interface.\n\n### `async`\n\nA controller around `promise`. The async function may be a plain function or a `MaybeReactive\u003cFn\u003e` (so the body itself can re-read signals and rerun on change).\n\n```ts\nimport { async } from \"elements-kit/utilities/async\";\n\nconst op = async(() =\u003e fetch(\"/api/items\").then((r) =\u003e r.json()));\n\nop.start();   // run with reactive tracking — reruns when tracked signals change\nawait op;     // awaitable (delegates to .then/.catch/.finally via .raw)\nop.stop();    // halt reruns + fire registered cleanup\n```\n\nReactive state getters: `.state`, `.value`, `.reason`, `.result`, `.pending`, `.raw` (the underlying `ComputedPromise`).\n\nOne-shot mutation (no tracking):\n\n```ts\nconst del = async((id: number) =\u003e\n  fetch(`/api/items/${id}`, { method: \"DELETE\" }).then((r) =\u003e r.json()),\n);\n\nawait del.run(42);\n```\n\n`Async` implements `Symbol.dispose`, so `using` auto-stops on scope exit:\n\n```ts\n{\n  using poll = async(() =\u003e fetch(\"/api/poll\").then((r) =\u003e r.json())).start();\n  await poll;\n} // poll.stop() here\n```\n\n### Composing with retry, online, storage\n\n`async`'s reactive body composes with other utilities. Below: fetch a todo by `id()`, retry on failure with exponential backoff, pause while offline (returning the stale cached value), and refetch when the tab regains focus.\n\n```ts\nimport { signal, effect, untracked, onCleanup } from \"elements-kit/signals\";\nimport { async } from \"elements-kit/utilities/async\";\nimport { retry } from \"elements-kit/utilities/retry\";\nimport { online } from \"elements-kit/utilities/network\";\nimport { windowFocused } from \"elements-kit/utilities/window-focus\";\nimport { createLocalStorage } from \"elements-kit/utilities/storage\";\n\nconst id = signal(1);\nconst cache = createLocalStorage\u003cunknown\u003e(\"todo-cache\", null);\n\nconst fetchTodo = async(() =\u003e {\n  if (!online()) return untracked(cache);   // pause while offline\n  windowFocused();                          // refetch on tab focus\n  return retry(() =\u003e {\n    const controller = new AbortController();\n    onCleanup(() =\u003e controller.abort());    // abort before each retry\n    return fetch(`/api/todos/${id()}`, { signal: controller.signal })\n      .then((r) =\u003e r.json())\n      .then((value) =\u003e (cache(value), value));\n  }, 3, (n) =\u003e n * 500)();                  // 0 ms, 500 ms, 1000 ms backoff\n}).start();\n\neffect(() =\u003e console.log(fetchTodo.state, fetchTodo.value));\n```\n\n## `For` — Keyed List Rendering\n\nReconciles a reactive array into the DOM. Each item renders once per key — no full re-renders on reorder, add, or remove. `T` is inferred from `each`.\n\n```tsx\nimport { For } from \"elements-kit/for\";\n\n\u003cul\u003e\n  \u003cFor each={todos} by={(todo) =\u003e todo.id}\u003e\n    {(todo) =\u003e (\n      \u003cli\u003e\n        \u003cinput type=\"checkbox\" checked={computed(() =\u003e todo.done)} on:change={() =\u003e (todo.done = !todo.done)} /\u003e\n        {todo.text}\n      \u003c/li\u003e\n    )}\n  \u003c/For\u003e\n\u003c/ul\u003e\n```\n\n## Prop types\n\nSix type helpers derive JSX prop shapes from your components — no parallel `declare global` block to maintain. Full guide at [docs/src/content/docs/elements/types.mdx](docs/src/content/docs/elements/types.mdx).\n\n| Helper | For |\n| ------ | --- |\n| `ElementProps\u003ctypeof Cls\u003e` | `HTMLElement` subclass — full surface (attrs, events, slots, children) |\n| `Props\u003cC\u003e` | Class instance, constructor, or function component — unified |\n| `ComponentProps\u003ctypeof Cls\u003e` | Class components with `constructor(props: P)` |\n| `MaybeReactiveProps\u003cP\u003e` | Caller-facing — wrap every prop in `MaybeReactive` (what parents pass) |\n| `ReactiveProps\u003cP\u003e` | Component-facing — every prop becomes a `Computed\u003cT\u003e` getter (what function components receive) |\n| `MaybeReactive\u003cT\u003e` | Scalar value-or-getter (from `elements-kit/signals`) |\n| `Require\u003cP, K\u003e` | Promote optional keys to required |\n\nThe JSX runtime auto-wraps function-component props — each key arrives as a callable getter that subscribes on read. Pair the signature with `ReactiveProps\u003cP\u003e` and read `props.x()`:\n\n```tsx\nimport type { ReactiveProps } from \"elements-kit/jsx-runtime\";\n\nfunction Greeting(props: ReactiveProps\u003c{ name: string }\u003e) {\n  return \u003cp\u003eHello, {props.name}\u003c/p\u003e;\n}\n```\n\n`resolveProps` stays exported for non-JSX call sites or nested prop bags.\n\n## `@reactive()` Decorator\n\nMakes any class field reactive — reads subscribe, writes trigger updates.\n\n```ts\nimport { reactive, computed } from \"elements-kit/signals\";\n\nclass TodoApp {\n  @reactive() todos: Todo[] = [];\n  @reactive() showDone = true;\n\n  visible = computed(() =\u003e\n    this.showDone ? this.todos : this.todos.filter((t) =\u003e !t.done),\n  );\n}\n```\n\n## `@attributes` Decorator\n\nWires `observedAttributes` and `attributeChangedCallback` from a static map:\n\n```ts\nimport { attributes, ATTRIBUTES as attr } from \"elements-kit/attributes\";\n\n@attributes\nclass MyElement extends HTMLElement {\n  static [attr] = {\n    value(this: MyElement, v: string | null) {\n      this.value = v ?? \"\";\n    },\n  };\n\n  @reactive() value = \"\";\n}\n```\n\nFor typed slots, attach a `[SLOTS]` instance field — pass the key list with `as const` so TS can narrow:\n\n```ts\nimport { SLOTS, Slots } from \"elements-kit/slot\";\n\nclass Card extends HTMLElement {\n  [SLOTS] = Slots.new([\"header\", \"footer\"] as const);\n}\n// ElementProps\u003ctypeof Card\u003e now includes `slot:header` / `slot:footer`\n```\n\nFor typed events, declare a `static events` map:\n\n```ts\nclass XPicker extends HTMLElement {\n  declare static events: { commit: CustomEvent\u003cnumber\u003e };\n}\n// ElementProps\u003ctypeof XPicker\u003e now includes `on:commit`\n```\n\n## Learn more\n\n- [Documentation site](docs/) — guides, playgrounds, reference\n- [Philosophy](docs/src/content/docs/getting-started/philosophy.mdx) — deeper reasoning behind the five principles\n- [ARCHITECTURE.md](ARCHITECTURE.md) — how the library works\n- [CONTRIBUTING.md](CONTRIBUTING.md) — build, test, PR checklist\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaelbettayeb%2Felements-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwaelbettayeb%2Felements-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaelbettayeb%2Felements-kit/lists"}