Gonia

February 17, 2026 · View on GitHub

A lightweight, SSR-first reactive UI library for building web applications with HTML-based templates and declarative directives.

Features

  • SSR-First Architecture - Server-side rendering with seamless client hydration
  • Declarative Directives - Vue-inspired template syntax (g-text, g-for, g-if, etc.)
  • Fine-Grained Reactivity - Efficient updates without virtual DOM diffing
  • Minimal Dependencies - Core library has no runtime dependencies (happy-dom for SSR only)
  • TypeScript Native - Full type safety with excellent IDE support

Installation

pnpm add gonia

Quick Start

Server-Side Rendering

import { render } from 'gonia/server';

// Importing directives registers them globally via directive()
import 'gonia/directives';

// Render HTML with state — globally registered directives are discovered automatically
const html = await render(
  '<ul><li g-for="item in items" g-text="item"></li></ul>',
  { items: ['Apple', 'Banana', 'Cherry'] },
  new Map()
);

Client-Side Hydration

import { hydrate } from 'gonia/client';
import { directive } from 'gonia';

// Import directives (registers globally)
import './directives/my-app.js';

// Hydrate when DOM is ready
hydrate();

Creating a Component Directive

Directives receive their dependencies through dependency injection — parameter names like $element and $scope tell the framework what to provide:

import { directive, Directive } from 'gonia';

const myApp: Directive<['$element', '$scope']> = ($element, $scope) => {
  // Initialize scope
  $scope.count = 0;

  // Define methods
  $scope.increment = () => {
    $scope.count++;
  };
};

// Register with scope: true to create isolated state
directive('my-app', myApp, { scope: true });
<my-app>
  <p g-text="count"></p>
  <button g-on="click: increment">+1</button>
</my-app>

Directives

DirectiveDescriptionExample
g-textSet text content<span g-text="message"></span>
g-showToggle visibility<div g-show="isVisible">...</div>
g-ifConditional render<p g-if="hasError">Error!</p>
g-forLoop iteration<li g-for="item in items">...</li>
g-classDynamic classes<div g-class="{ active: isActive }">
g-modelTwo-way binding<input g-model="name">
g-onEvent handling<button g-on="click: handleClick">
g-scopeInline scope init<div g-scope="{ count: 0 }">
g-bind:*Dynamic attributes<a g-bind:href="link">
g-templateLoad named template<div g-template="dialog">
g-slotContent slot projection<slot g-slot="activeTab">

Vite Integration

// vite.config.ts
import { defineConfig } from 'vite';
import { gonia } from 'gonia/vite';

export default defineConfig({
  plugins: [gonia()]
});

Documentation

See the docs folder for detailed documentation:

Roadmap

Done

  • Core directives (g-text, g-show, g-if, g-for, g-class, g-model, g-on, g-scope, g-bind:*, g-html)
  • Template directives (g-template, g-slot, slot content projection)
  • Directive options (scope, template, assign, provide, using)
  • SSR with client hydration
  • Vite plugin with $inject transformation
  • Typed context registry
  • Persistent scopes for g-if toggles
  • Async directives with suspense boundaries (fallback, ssr modes, streaming)

Planned

  • Reducer-based two-way bindings (scope: { prop: '=' })
  • Scoped CSS with automatic class mangling
  • Browser devtools extension
  • Transition system for g-if/g-for

License

MIT