Getting Started
Install the CLI, scaffold a project, run it. About a minute later a GTK 4 window is on screen, running your own TypeScript.
Every command block on this page has a tab per runtime — GJS, Node.js, Bun, Deno — and the tab you pick follows you across the rest of the site.
What you need
Section titled “What you need”One JavaScript runtime, and the GNOME libraries your app uses. On Linux, GTK 4 and libadwaita come from your distribution.
| Runtime | Version | Reaches |
|---|---|---|
| GJS | 1.86 or newer | Linux, macOS in part |
| Node.js | 20 or newer | Linux, macOS, Windows |
| Bun | current release | Linux, macOS, Windows |
| Deno | 2 | Linux, macOS, Windows |
On macOS and Windows the Node, Bun and Deno route brings its own GTK, so there is nothing to install by hand.
Install the CLI
Section titled “Install the CLI”curl -fsSL https://github.com/gjsify/gjsify/releases/latest/download/install.mjs \ -o /tmp/g.mjs && gjs -m /tmp/g.mjs && rm /tmp/g.mjsnpm install -g @gjsify/clibun add -g @gjsify/clideno install -g -A -n gjsify npm:@gjsify/cliAll four give you the same gjsify command, and the runtime you installed with
becomes the one it builds for by default. Each route puts the launcher somewhere
different, so add that directory to your PATH if the shell cannot find it:
~/.local/bin after the GJS bootstrap, your npm prefix after npm, ~/.bun/bin after
Bun, ~/.deno/bin after Deno.
export PATH="$HOME/.local/bin:$PATH"Updating, pinning a version, adding the CLI to one project instead of globally, and uninstalling are on the Install & Update page.
Create and run your first app
Section titled “Create and run your first app”gjsify create hello-gjsify --template gtk-minimalcd hello-gjsifygjsify installgjsify run devnpm create @gjsify/app@latest hello-gjsify -- --template gtk-minimalcd hello-gjsifynpm installnpm run buildnpm run start:nodebunx @gjsify/cli@latest create hello-gjsify --template gtk-minimal --runtime buncd hello-gjsifybun installbun run buildbun run start:bundeno run -A --reload --min-dep-age=0 npm:@gjsify/cli@latest \ create hello-gjsify --template gtk-minimal --runtime denocd hello-gjsifydeno installdeno task builddeno task start:denoA window titled hello-gjsify opens with a label in it. That window is your
src/index.ts, bundled into one file and executed by the runtime you picked.
Add a button
Section titled “Add a button”Open src/index.ts. It is plain GTK reached through gi://, with no gjsify
abstraction in the way:
import Gtk from 'gi://Gtk?version=4.0';import Gio from 'gi://Gio?version=2.0';
const app = new Gtk.Application({ applicationId: 'org.gjsify.hello-gjsify', flags: Gio.ApplicationFlags.FLAGS_NONE,});Nothing in that file names a runtime. The same bytes build for GJS, Node.js, Bun and Deno, and so does every TypeScript sample on this site, the widget gallery included.
applicationId is the session-bus name your app claims, and every project needs its
own. Give each project its own: when two running apps claim one name, GTK treats the
second launch as a remote instance of the first and exits 0 with no window of its own.
Give the label a name in src/main-window.blp so TypeScript can reach it, and
add the button beside it. The caption goes through _() because a string set
in a template is what xgettext can see; a string set from TypeScript is not:
Gtk.Label title { label: _("Hello from gjsify!");}
Gtk.Button hello { label: _("Say hello");
styles [ "suggested-action", ]}Then wire the click in src/main-window.ts, naming both children in
InternalChildren and keeping the handler in the constructor:
export class MainWindow extends Gtk.ApplicationWindow { declare private _hello: Gtk.Button; declare private _title: Gtk.Label;
static { GObject.registerClass( { GTypeName: 'MainWindow', Template, InternalChildren: ['hello', 'title'], }, this, ); }
constructor(application: Gtk.Application) { super({ application }); this._hello.connect('clicked', () => this._title.set_label('Hello yourself!')); }}The split is the point: the template declares what the window is, TypeScript
decides what it does. A list whose length is not known until runtime is
still filled from TypeScript — declare the container in the .blp and append to
it here.
Build and start again and the button is there. For the Adwaita widgets (header bars, boxed lists, toasts, view switchers) see the Widget Gallery, and for a full app shell with navigation and dialogs see Native Adwaita Apps.
Use Node.js and Web APIs
Section titled “Use Node.js and Web APIs”Write ordinary Node.js and Web code in the same file. The build resolves each
node:* import and injects the globals your code touches, so you maintain no
import list of your own:
import { readFileSync } from 'node:fs'import { createServer } from 'node:http'
const html = readFileSync('index.html', 'utf-8')
const server = createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }) res.end(html)})
const port = Number(process.env.PORT ?? 8080)server.listen(port, () => { console.log(`Server running on http://localhost:${port}`)})Where each import lands depends on the target, and that is the whole difference.
In a --app gjs bundle it lands on gjsify’s implementation, each one backed by a
GNOME library: node:fs on Gio.File, node:http on Soup.Server, process.env
on GLib.getenv(). In a --app node bundle it lands on Node, Bun or Deno’s own,
because all three already have it. Your source does not change.
Web APIs work the same way:
const response = await fetch('https://api.example.com/data')const data = await response.json()
const ws = new WebSocket('wss://echo.example.com')ws.addEventListener('message', (event) => console.log(event.data))Packages lists what is implemented.
If it did not start
Section titled “If it did not start”Ask the CLI what is missing:
gjsify system-checknpx @gjsify/cli@latest system-checkbunx @gjsify/cli@latest system-checkdeno run -A npm:@gjsify/cli@latest system-checkIt prints a line per dependency, names the @gjsify/* package that wants each
optional one, and warns when your @girs/* types describe a newer library than the
one installed. It checks one fixed list, gjs included.
Next steps
Section titled “Next steps”- Widget Gallery: every Adwaita component, with live previews
- Native Adwaita Apps: navigation, dialogs, toasts, the app shell
- Runtimes: build the same source for GJS, Node.js, Bun, Deno or the browser
- Ship a Flatpak: put your app on Flathub
- One-line installer: let users install your app with
curl - CLI Reference: every command and flag
- Packages: the Node.js, Web and DOM APIs gjsify implements