Skip to content

Learn Gluon step by step ​

This path assumes basic TypeScript, HTML, and npm knowledge. It does not assume framework experience. Complete the steps in order; each one adds a single new idea.

The mental model ​

Keep these five facts in mind:

  1. html returns a description of DOM, called a TemplateResult.
  2. A ref owns one reactive value. Reading .value while rendering creates the dependency; writing .value schedules the update.
  3. Gluon updates bindings inside existing DOM when the template callsite stays the same.
  4. A Gluon application owns effects and cleanup until unmount().
  5. Browser standards stay visible: events are native events, Custom Elements are real Custom Elements, and styles are constructable CSSStyleSheets.

Step 1: create and run a project ​

sh
npm create gluon@latest my-shop
cd my-shop
npm install
npm run dev

Open the URL printed by Vite. The generated project contains:

FileJunior-friendly purpose
index.htmlProvides the mount element such as <div id="app"></div>.
src/main.tsCreates state, returns the root template, and mounts the app.
vite.config.tsActivates the official Gluon transform and diagnostics.
package.jsonLists the run, build, typecheck, and test commands.

Run npm run build before committing. A successful dev page alone does not prove that TypeScript and the production bundle are valid.

Step 2: read a template ​

ts
const name = 'Orbit Lamp';
const price = 128;

html`
  <article>
    <h2>${name}</h2>
    <p>$${price}</p>
  </article>
`;

Static markup remains normal HTML. ${...} is a binding. Do not build HTML by concatenating strings; Gluon escapes ordinary values and updates the exact binding.

The most common binding forms are:

SyntaxMeaningExample
${value}Text, child template, node, or list content<p>${message}</p>
name=${value}String attributearia-label=${label}
.name=${value}JavaScript property.product=${product}
?name=${value}Boolean attribute?disabled=${saving.value}
@name=${listener}Native event listener@click=${save}

Use a property binding for objects, arrays, or live DOM properties such as input.value. Use an attribute for serializable HTML text.

Step 3: add state ​

ts
import { createApp, html } from '@gluonjs/core';
import { ref } from '@gluonjs/reactivity';

const count = ref(0);

createApp(() => html`
  <button @click=${() => { count.value += 1; }}>
    Added ${count.value} time(s)
  </button>
`).mount(document.querySelector('#app')!);

The event writes the state; the template reads it. Do not call render() manually after changing a ref.

Step 4: render a stable list ​

Use repeat() when items have stable identities:

ts
repeat(
  products,
  (product) => product.id,
  (product) => html`<article>${product.name}</article>`,
);

The key must be unique and stable. An array index is unsuitable when items can be reordered or removed because the index describes a position, not the item.

Step 5: own styles and cleanup ​

Gluon intentionally uses constructable stylesheets. A StyleSheetOwner retains only the sheets it owns and releases them during application cleanup:

ts
const owner = createStyleSheetOwner(document);
owner.retain(css`main { max-inline-size: 40rem; }`);
app.onUnmounted(() => owner.dispose());

The full compiled feature below combines typed data, a controlled search input, derived state, a keyed list, stylesheet ownership, and page cleanup:

ts
import {
  createApp,
  createStyleSheetOwner,
  css,
  html,
  repeat,
} from '@gluonjs/core';
import {
  computed,
  ref,
} from '@gluonjs/reactivity';

interface Product {
  readonly id: string;
  readonly name: string;
  readonly price: string;
}

const products: readonly Product[] = [
  { id: 'orbit-lamp', name: 'Orbit Lamp', price: '$128' },
  { id: 'field-tote', name: 'Field Tote', price: '$84' },
];
const query = ref('');
const visibleProducts = computed(() => {
  const needle = query.value.trim().toLowerCase();
  return needle
    ? products.filter((product) => product.name.toLowerCase().includes(needle))
    : products;
});
const styles = css`
  main { max-inline-size: 40rem; margin: 2rem auto; font: 1rem/1.5 system-ui; }
  label, article { display: grid; gap: 0.5rem; }
  section { display: grid; gap: 1rem; margin-block-start: 1.5rem; }
  article { padding: 1rem; border: 1px solid #d8d8d8; }
`;
const styleOwner = createStyleSheetOwner(document);
styleOwner.retain(styles);

const app = createApp(() => html`
  <main>
    <h1>Products</h1>
    <label>
      Search
      <input
        type="search"
        .value=${query.value}
        @input=${(event: Event) => {
          query.value = (event.currentTarget as HTMLInputElement).value;
        }}
      >
    </label>
    <p>${visibleProducts.value.length} result(s)</p>
    <section>
      ${repeat(
        visibleProducts.value,
        (product) => product.id,
        (product) => html`
          <article>
            <strong>${product.name}</strong>
            <span>${product.price}</span>
          </article>
        `,
      )}
    </section>
  </main>
`);

app.onUnmounted(() => styleOwner.dispose());
const mounted = app.mount(document.querySelector('#app')!);
window.addEventListener('pagehide', () => mounted.unmount(), { once: true });

Step 6: test public behavior ​

Test through the rendered surface:

ts
import { html } from '@gluonjs/core';
import { cleanupFixtures, mountComponent } from '@gluonjs/test-utils';

const fixture = mountComponent(
  ({ label }: Readonly<{ label: string }>) => html`<button>${label}</button>`,
  { props: { label: 'Save' } },
);

if (fixture.get('button').textContent !== 'Save') throw new Error('Expected Save');
await cleanupFixtures();

Prefer queries and assertions a user or platform consumer can observe. Do not assert renderer comments, private fields, or implementation classes.

Step 7: choose the next guide ​

Before the advanced authoring comparison, complete Build one stateful component. It connects a public input, local state, native output, render, lifecycle, cleanup, parent usage, and a testable browser boundary in one small example.

Common first-project mistakes ​

SymptomCheck
The page is emptyConfirm index.html has the queried mount ID and that mount() receives that element.
State changed but the UI did notRead the ref as .value inside the app render function; do not copy it to a non-reactive variable first.
An object becomes [object Object]Use .property=${value} rather than an attribute binding.
A checkbox or disabled control is wrongUse ?checked=${value} or ?disabled=${value} for boolean attributes.
A list keeps the wrong row after removalGive repeat() a stable domain key instead of the array index.
TypeScript accepts dev code but production failsRun npm run build; do not rely only on the dev server.
Styles disappear or leak between surfacesRetain constructable sheets with an explicit owner and dispose that owner.
Event code survives after navigationPrefer template event bindings, or register imperative listeners with owner cleanup.
A component has hidden state but no lifecycleMove the stateful boundary to defineGluonElement() or GluonElement.

Every public symbol also has a compiled, task-oriented example in the API reference. Use the Cookbook when you know the task and the API reference when you know the symbol.

Small glossary ​

TermMeaning
TemplateA TemplateResult created by html or svg.
BindingOne dynamic value inside a template.
RefA reactive object whose current value is .value.
ComponentA function that returns a template, optionally with layer metadata.
Custom ElementA registered browser element with its own host and lifecycle.
MountStart an application in a specific container.
UnmountStop effects, release owned resources, and remove renderer DOM.
QuarkA thin native element or headless behavior building block.
AtomOne reusable presentational control or value.
MoleculeA small composition of related parts.
OrganismA larger reusable section or workflow boundary.

Version-matched documentation. Examples are checked against the public package contracts.