Gyral 0.3.10

Model-View-Intent web components on the modern web platform.

Your app is a pure function, and side effects happen at the edges, as data. Gyral builds that loop on custom elements, semantic HTML, modern CSS and a small view layer of its own, with no runtime dependencies.

pnpm add @gyral/core

One loop, three pure parts

Events become messages, messages become state, state becomes HTML. That's the whole app.

import { define, html } from '@gyral/core';

type Msg = { _tag: 'Increment' } | { _tag: 'Decrement' };

export const Counter = define<{ count: number }, Msg>()('my-counter', {
  init: () => ({ count: 0 }),
  // Intent: parse platform events into typed messages.
  intent: {
    Increment: () => ({ _tag: 'Increment' }),
    Decrement: () => ({ _tag: 'Decrement' }),
  },
  // Model: one pure reducer per message, exhaustive by type.
  update: {
    Increment: (s) => ({ count: s.count + 1 }),
    Decrement: (s) => ({ count: s.count - 1 }),
  },
  // View: a pure template that names intents. No closures.
  view: (s, i) => html`
    <button type="button" data-intent=${i.Decrement}>−</button>
    <output>${s.count}</output>
    <button type="button" data-intent=${i.Increment}>+</button>
  `,
});
A complete component. The view names intents; it never holds a handler.
Live: server-rendered, then hydrated in place. Click and watch the loop.

See what it makes easy

Every interaction is data, so undo, race-free search and pages that work before JavaScript take a few lines. Watch the demos.

Built on the platform, not around it

Pure means testable

Update and view are plain functions, so you test behaviour by calling them. When you need the browser, @gyral/testing mounts real components in Chromium, renders them on the server and checks they hydrate, and runs timers on a virtual clock.

import { run, step } from '@gyral/testing';
import { Counter } from './counter.js';

// No DOM, no mocks: the model is a function.
const inc = { _tag: 'Increment' } as const;

step(Counter.spec, { count: 1 }, inc).state;
// → { count: 2 }

run(Counter.spec, [inc, inc, inc]).state;
// → { count: 3 }

One store, four looks

gyral-shop is a full department store built with Gyral: catalog, search, accounts, cart, checkout, orders and admin. Its themes change only CSS. The markup stays the same, the way the CSS Zen Garden proved it could. Try the live demo.

Inspired by Cycle.js

Cycle.js showed that a web app can be one visible loop with every effect at the edge. Gyral is a new framework that carries that idea forward. It replaces streams with plain functions and the virtual DOM with the platform's own components, and credits the original in its license notice.

Build your first component

Install one package and write one function. The guide takes about ten minutes.

Read the guide