Skip to content

GJSify

GTK 4 & Adwaita everywhere.
In type-safe TypeScript.

Write one GTK 4 / Adwaita app in TypeScript and ship it for Linux, macOS and Windows, on GJS, Node.js, Bun or Deno. No embedded browser.

Terminal
$ gjs -m express-webserver.js
Express.js blog running on GJS
➜ Local: http://localhost:3000/
➜ API: http://localhost:3000/api/posts
Press Ctrl+C to stop
[12:34:56] GET /
[12:34:56] GET /style.css
[12:34:56] GET /app.js
[12:34:56] GET /api/runtime
[12:34:56] GET /api/posts
express-webserver.ts
import '@gjsify/node-globals';
import express from 'express';
import { posts } from './data/posts.js';
const app = express();
app.use(express.json());
app.get('/api/posts', (_req, res) => {
res.json({ count: posts.length, posts });
});
app.get('/api/posts/:slug', (req, res) => {
const post = posts.find((p) => p.slug === req.params.slug);
if (!post) return res.status(404).json({ error: 'Not found' });
res.json(post);
});
app.use(express.static('public'));
app.listen(3000, () => {
console.log('Express server running at http://localhost:3000');
});
Full example →
Terminal
Terminal
gjsify showcase express-webserver
Terminal
npx @gjsify/cli@latest showcase express-webserver --runtime node
Terminal
bunx @gjsify/cli@latest showcase express-webserver --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase express-webserver --runtime deno
switch-row.meta.ts
import { ControlType, type StoryMeta } from '@gjsify/stories';
// Author a widget's story once — renderer-free metadata + controls.
// The native GTK storybook (gjsify storybook) and the browser one
// both render it from this same source, so the two stay in lockstep.
export const switchRowMeta: StoryMeta = {
title: 'Boxed Lists/Switch Row',
description: 'Adw.SwitchRow — a row with a trailing switch.',
controls: [
{ name: 'title', label: 'Title', type: ControlType.TEXT, defaultValue: 'Automatic updates' },
{ name: 'active', label: 'Active', type: ControlType.BOOLEAN, defaultValue: true },
],
};
Full example →
Terminal
Terminal
gjsify showcase adwaita-storybook
Terminal
npx @gjsify/cli@latest showcase adwaita-storybook --runtime node
Terminal
bunx @gjsify/cli@latest showcase adwaita-storybook --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase adwaita-storybook --runtime deno
pixel-demo.ts
import Adw from 'gi://Adw?version=1';
import { WebGLBridge } from '@gjsify/webgl';
import * as THREE from 'three';
import { RenderPixelatedPass } from 'three/addons/postprocessing/RenderPixelatedPass.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
const app = new Adw.Application({ application_id: 'gjsify.examples.pixel' });
app.connect('activate', () => {
const glArea = new WebGLBridge();
glArea.onReady((canvas) => {
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera();
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPixelatedPass(6, scene, camera));
});
});
Full example →
Terminal
Terminal
gjsify showcase three-postprocessing-pixel
Terminal
npx @gjsify/cli@latest showcase three-postprocessing-pixel --runtime node
Terminal
bunx @gjsify/cli@latest showcase three-postprocessing-pixel --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase three-postprocessing-pixel --runtime deno
teapot-demo.ts
import Adw from 'gi://Adw?version=1';
import { WebGLBridge } from '@gjsify/webgl';
import * as THREE from 'three';
import { TeapotGeometry } from 'three/addons/geometries/TeapotGeometry.js';
const app = new Adw.Application({ application_id: 'gjsify.examples.teapot' });
app.connect('activate', () => {
const glArea = new WebGLBridge();
glArea.onReady((canvas) => {
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45);
scene.add(new THREE.Mesh(
new TeapotGeometry(50, 20),
new THREE.MeshPhongMaterial({ color: 0xc0c0c0 })
));
});
});
Full example →
Terminal
Terminal
gjsify showcase three-geometry-teapot
Terminal
npx @gjsify/cli@latest showcase three-geometry-teapot --runtime node
Terminal
bunx @gjsify/cli@latest showcase three-geometry-teapot --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase three-geometry-teapot --runtime deno
ldraw-demo.ts
import Adw from 'gi://Adw?version=1';
import { WebGLBridge } from '@gjsify/webgl';
import * as THREE from 'three';
import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js';
const app = new Adw.Application({ application_id: 'gjsify.examples.ldraw' });
app.connect('activate', () => {
const glArea = new WebGLBridge();
glArea.onReady((canvas) => {
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45);
new LDrawLoader().load('models/car.ldr_Packed.mpd', (model) => {
model.rotation.x = Math.PI;
scene.add(model);
});
});
});
Full example →
Terminal
Terminal
gjsify showcase three-loader-ldraw
Terminal
npx @gjsify/cli@latest showcase three-loader-ldraw --runtime node
Terminal
bunx @gjsify/cli@latest showcase three-loader-ldraw --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase three-loader-ldraw --runtime deno
jelly-jumper.ts
import Adw from 'gi://Adw?version=1';
import { WebGLBridge } from '@gjsify/webgl';
import * as ex from 'excalibur';
import { startGame } from './game.js';
const app = new Adw.Application({
application_id: 'gjsify.examples.jelly-jumper',
});
app.connect('activate', () => {
const glArea = new WebGLBridge();
glArea.onReady(async (canvas) => {
const game = await startGame(canvas);
// game.mute(); // toggle audio
// game.pause(); // toggle pause
});
});
Full example →
Terminal
Terminal
gjsify showcase excalibur-jelly-jumper
Terminal
npx @gjsify/cli@latest showcase excalibur-jelly-jumper --runtime node
Terminal
bunx @gjsify/cli@latest showcase excalibur-jelly-jumper --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase excalibur-jelly-jumper --runtime deno
fireworks.ts
import Adw from 'gi://Adw?version=1';
import { Canvas2DBridge } from '@gjsify/canvas2d';
import { start } from './fireworks.js';
const app = new Adw.Application({ application_id: 'gjsify.examples.fireworks' });
app.connect('activate', () => {
const canvasWidget = new Canvas2DBridge();
canvasWidget.installGlobals();
canvasWidget.onReady((canvas) => {
const demo = start(canvas);
// demo.effectController.particleCount = 60;
// demo.effectController.autoInterval = 150;
});
});
Full example →
Terminal
Terminal
gjsify showcase canvas2d-fireworks
Terminal
npx @gjsify/cli@latest showcase canvas2d-fireworks --runtime node
Terminal
bunx @gjsify/cli@latest showcase canvas2d-fireworks --runtime bun
Terminal
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest showcase canvas2d-fireworks --runtime deno

One app. Every desktop.

The same TypeScript, the same Adwaita widgets, the same gjsify commands. GJSify talks to the real GTK 4 libraries on each system — no embedded browser, no per-platform rewrite.

GNOME Native GJS on the system libadwaita.

The same application is shown running on GNOME, Windows 11, macOS.

Get started in seconds

Terminal
Terminal
# See every command, from create to ship
gjsify --help
# Scaffold a new app
gjsify create my-app
Terminal
# See every command, from create to ship
npx @gjsify/cli@latest --help
# Scaffold a new app
npx @gjsify/cli@latest create my-app
Terminal
# See every command, from create to ship
bunx @gjsify/cli@latest --help
# Scaffold a new app
bunx @gjsify/cli@latest create my-app
Terminal
# See every command, from create to ship
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest --help
# Scaffold a new app
deno run -A --reload --min-dep-age 0 npm:@gjsify/cli@latest create my-app