.js docs
GitHub
Apis /ˈeɪpɪs/ · Latin, "bee" — small units, compiled into one efficient structure

A UI compiler that
disappears at build time.

Apis.js components are single files of standard HTML, CSS and JavaScript. There's no virtual DOM and no framework runtime shipped to the browser — the compiler reads your component once and writes out the plain DOM calls that would have been tedious to hand-write.

You write
<script>
  let name = 'world';
  function rename() {
    name = 'user';
  }
</script>

<h1>Hello {name.toUpperCase()}!</h1>
<button @click={rename}>Rename</button>
Apis.js compiles to
export default $runtime.makeComponent($option => {
  const $$apply = $runtime.makeApply();
  let name = 'world';
  function rename() {
    $$apply();
    name = 'user';
  }
  const $el = $runtime.htmlToFragment(
    `<h1> </h1><button>Rename</button>`, 1);
  let [t, h1, btn] = $runtime.refer($el, '++a1.');
  $runtime.bindText(t, () => `Hello ${name.toUpperCase()}!`);
  $runtime.addEvent(btn, 'click', () => { rename(); $$apply(); });
  return $el;
});

No virtual DOM diffing, no proxies wrapping your state. Reactivity is a small digest loop: every event handler ends by calling $$apply(), which re-runs the bindings that were registered during render and only touches the DOM where a value actually changed.

Assignment-driven

Write count++, not setState.

Zero runtime VDOM

Compiles to direct createElement/appendChild calls.

Standard syntax

HTML, CSS and JS — no new file format to learn.

Scoped CSS by default

:global() escape hatch when you need it.

Introduction

Quick example

A small counter with a store-backed value, a reactive statement, and a transition — five of the ideas on this page in one file.

Counter.xht
<script>
  import { writable } from 'apisjs/runtime.js';

  const count = writable(0);
  let visible = true;

  $: isEven = count.value % 2 === 0;

  function inc() { count.update(n => n + 1); }
</script>

<button @click={inc}>Count: {count.value}</button>

{#if visible}
  <p transition:fade={{ duration: 200 }} class:even={isEven}>
    {isEven ? 'Even' : 'Odd'}
  </p>
{/if}

<style>
  .even { color: seagreen; }
</style>
Getting started

Anatomy of a component

Every component is one file with up to three top-level blocks. None are required — a template-only file is a valid component.

BlockPurpose
<script>State, functions, imports, lifecycle hooks, export let props
TemplateEverything outside <script>/<style> — markup, interpolation, control flow
<style>CSS, scoped to this component by default

Compile a source string programmatically with compile(source, config):

import { compile } from 'apisjs';

const ctx = await compile(componentSource, { cssGenId: () => `c${id++}` });
ctx.result;     // compiled JS module, as a string
ctx.css.result; // extracted, scoped CSS, if any

Build tooling

In practice you don't call compile() by hand — a bundler plugin does it as part of your build.

rollup.config.js
import apis from 'apisjs/apis-rollup.js';

export default {
  input: 'src/main.js',
  plugins: [
    apis({ extension: ['xht', 'html'] })
  ]
};

An esbuild-based dev workflow (apis-esbuild.js) is also included, with optional live-reload via an esbuild.config.js/derver.config.js pair in your project root.

Template syntax

Interpolation

Curly braces embed any JavaScript expression in text. It's re-evaluated on every digest, so it always reflects current state — no manual re-render call.

<h1>Hello {name.toUpperCase()}!</h1>
<p>{items.length} item{items.length === 1 ? '' : 's'}</p>

Elements & attributes

Attributes accept a static string, an {expr}, or a bare boolean flag.

<img src={avatarUrl} alt="Profile photo" />
<button disabled>Can't click me</button>
<button disabled={isSaving}>Save</button>
Control flow

Conditionals

{#if user}
  <p>Welcome, {user.name}</p>
{:else if loading}
  <p>Loading…</p>
{:else}
  <p>Please sign in</p>
{/if}

Elements entering or leaving an {#if} block are exactly where transition:/in:/out: attach — the block already waits for an outro to finish before removing the DOM node.

Loops

Destructure the item, grab the index, and give each row a stable key so reordering moves nodes instead of rebuilding them.

{#each todos as {text, done}, index (text)}
  <li class:done>{index + 1}. {text}</li>
{:else}
  <li>No todos yet</li>
{/each}

Async

{#await fetchUser(id)}
  <p>Loading…</p>
{:then user}
  <p>{user.name}</p>
{:catch error}
  <p class="error">{error.message}</p>
{/await}
Directives

Events

Modifiers chain after a pipe: stop, prevent, ctrl/alt/shift/meta, and key filters like enter/esc/arrow keys.

<button @click={inc}>+1</button>
<form @submit|stop|prevent={save}>…</form>
<input @keyup|enter={search} />

Two-way binding

<input :value={name} />
<input type="checkbox" :checked={agreed} />
<input type="number" :value={age} />

Actions — use:

An action is a plain function that runs once when its element mounts, and optionally returns a cleanup function. It's the escape hatch for imperative DOM behavior that doesn't warrant a whole child component.

tooltip.js
export function tooltip(node, message) {
  const show = () => node.setAttribute('title', message);
  show();
  return () => node.removeAttribute('title');
}
<div use:tooltip={message}>Hover me</div>
Directives new

Transitions

transition: plays both ways; in:/out: play one direction only. Four built-ins ship in the runtime: fade, fly, scale, slide — each is a plain function returning { duration, delay, easing, css }, so writing your own is the same shape.

<script>
  import { fade, fly } from 'apisjs/runtime.js';
  let visible = true;
</script>

{#if visible}
  <p transition:fade={{ duration: 200 }}>Hello</p>
{/if}

<div in:fly={{ y: -20 }} out:fade>Toast</div>

Outro transitions don't need special-casing in #if/#each — they hook into share.destroyResults, the same promise-based "wait before removing" mechanism those blocks already use for async cleanup. Return a promise from an $onDestroy callback and removal waits for it.

Class & style shorthands

<div class:active class:disabled={isDisabled}>…</div>
<div style:color={theme.color} style:font-size="14px">…</div>
Reactivity & state

The digest loop

There's no compile-time tracking of which statements touch which variables. Instead, every compiled @event handler ends with $$apply():

$runtime.addEvent(el, 'click', () => { inc(); $$apply(); });

$$apply() walks the component's change-detector tree and re-runs every registered binding's getter ($watch(fn, cb)); a getter whose return value changed fires its callback — a DOM update, a prop push into a child, a store write. It's dirty-checking, not a virtual DOM diff: no tree reconciliation, just "did this specific expression's value change."

Reactive statements — $:

let count = 0;
$: doubled = count * 2;
$: if (count > 10) console.log('big!');
Reactivity & state new

Stores

Any imported value with a .subscribe(fn) method is auto-subscribed by the compiler — subscribing re-runs the component's digest whenever the store notifies. writable/readable/derived implement that contract so you don't hand-roll it.

store.js
import { writable, derived } from 'apisjs/runtime.js';

export const count = writable(0);
export const doubled = derived(count, n => n * 2);
Counter.xht
<script>
  import { count, doubled } from './store.js';
</script>

<button @click={() => count.update(n => n + 1)}>
  {count.value}
</button>
<p>Doubled: {doubled.value}</p>

Templates read .value directly rather than unwrapping via a $store sugar — one less piece of magic to learn.

Context

$context is available in every component's script without an import — a plain object shared down the component tree, for state that would otherwise mean prop-drilling through several layers.

// anywhere in the subtree
$context.theme = 'dark';
console.log($context.theme);
Components

Props & slots

export let declares a reactive prop; export const declares one that's read once and never pushed to again.

Card.xht
<script>
  export let title;
  export const variant = 'default';
</script>

<div class="card {variant}">
  <h3>{title}</h3>
  {#slot}{/slot}
</div>
<Card title="Hello">
  <p>Card body content</p>
</Card>

Named, scoped slots pass a prop back into the parent's slot content:

List.xht
{#each items as item}
  {#slot:row item}{/slot}
{/each}
<List items={items}>
  {#slot:row item}
    <li>{item.label}</li>
  {/slot}
</List>
Styling

Scoped CSS

Styles in <style> are scoped to the component automatically — no BEM, no CSS-in-JS. Reach outside with :global().

<div class="wrap">{#slot}{/slot}</div>

<style>
  .wrap { padding: 1rem; }
  :global(body) { margin: 0; }
</style>
Lifecycle

Mount, destroy & tick

$onMount(() => {
  console.log('mounted');
  return () => console.log('cleanup, runs on destroy');
});

$onDestroy(() => console.log('destroyed'));

$tick().then(() => console.log('DOM has been updated'));
Status

Roadmap & TypeScript

Everything above this line is implemented and has been verified by actually compiling and running the example. Two things worth calling out explicitly:

ItemStatus
Stores (writable/readable/derived)Shipped
Transitions (transition:/in:/out:)Shipped
animate: for FLIP list reorderingPlanned
TypeScript (<script lang="ts">)Planned — type erasure only
SSR / hydrationNot started

The TypeScript plan is type erasure, not in-compiler type-checking: strip types at compile time (via the typescript package's transpileModule) and hand the resulting plain JS to the existing parser unchanged. Real type-checking stays the editor's or tsc's job — the same division Svelte and Vue SFCs use.