How to Implement Keyboard Shortcuts and Chrome Shortcuts in the Native SDK
The Native SDK exposes two distinct shortcut mechanisms—standard keyboard shortcuts routed through on_command and chrome shortcuts handled via on_key—both declared in app.zon and registered with the host OS through platform.services.configureShortcuts.
The vercel-labs/native repository provides a declarative way to implement keyboard shortcuts and chrome shortcuts in Native SDK applications through manifest-driven configuration and runtime callbacks. By defining Shortcut entries in your app.zon and wiring them to UiApp.Options, you can handle global commands and window-level chrome events without blocking text input widgets.
Declaring Keyboard and Chrome Shortcuts in the App Manifest
All shortcuts begin as declarations in the app.zon manifest file. The src/tooling/manifest.zig parser validates these entries via validateShortcut and stores them in AppInfo.shortcuts before the platform registers them with the operating system【/cache/repos/github.com/vercel-labs/native/main/src/tooling/manifest.zig#L49-L57】【/cache/repos/github.com/vercel-labs/native/main/src/platform/types.zig#L22-L27】.
Understanding the Shortcut Struct
In src/platform/types.zig, the native_sdk.Shortcut struct defines the required fields for every shortcut entry:
pub const Shortcut = struct {
id: []const u8, // command identifier (must match a command in the manifest)
key: []const u8, // printable key name or a special name like "escape"
modifiers: ShortcutModifiers = .{}, // at least one modifier is required for character keys
};
Source: src/platform/types.zig lines 82–99【/cache/repos/github.com/vercel-labs/native/main/src/platform/types.zig#L82-L99】.
A companion definition in src/primitives/app_manifest/types.zig mirrors the public struct for manifest parsing and enforces the max_shortcuts limit.
Adding Shortcuts to app.zon
Define your shortcuts under the shortcuts array inside app.zon:
shortcuts: [
{ id = "command.palette", key = "k", modifiers = [ "command" ] },
{ id = "command.clear", key = "escape" },
]
Each entry maps a key combination to a command id that the runtime will later forward to your application logic.
Wiring Keyboard and Chrome Shortcuts to Application Logic
The runtime delivers shortcut events through two separate callbacks inside UiApp.Options. Choosing the correct callback depends on whether the shortcut is a standard command or a chrome shortcut.
Using on_command for Standard Keyboard Shortcuts
Keyboard shortcuts are global shortcuts that fire when the user presses a registered key combination, such as cmd+K. The runtime translates the platform shortcut event into a native_sdk.Shortcut struct and forwards it to the app via the on_command callback.
In src/runtime/ui_app.zig, the UiApp.Options struct stores the on_command field around lines 78–81【/cache/repos/github.com/vercel-labs/native/main/src/runtime/ui_app.zig#L78-L81】:
const options = UiApp.Options{
.name = "MyApp",
.scene = ...,
.canvas_label = "main",
.on_command = onCommand,
};
fn onCommand(name: []const u8) ?Msg {
return switch (name) {
"command.palette" => Msg{ .ShowPalette = {} },
"command.clear" => Msg{ .Clear = {} },
else => null,
};
}
The callback receives the command id string defined in the manifest id field.
Using on_key for Chrome Shortcuts
Chrome shortcuts are platform-specific window-level controls, such as play/pause or back gestures. According to the precedence comment in src/runtime/ui_app.zig lines 31–38, these events are not routed through the widget hierarchy. Instead, they are exposed through the on_key fallback only after the widget tree has declined the key【/cache/repos/github.com/vercel-labs/native/main/src/runtime/ui_app.zig#L31-L38】.
This design guarantees that chrome shortcuts never steal typed text from input fields.
const options = UiApp.Options{
// ...
.on_key = onKey,
};
fn onKey(event: canvas.WidgetKeyboardEvent) ?Msg {
// `event.key` will be "space" etc.; modifiers are already forced to true.
return switch (event.key) {
"space" => Msg{ .TogglePlay = {} },
else => null,
};
}
Because modifiers are already forced to true for these events, you only need to match against event.key.
Platform Registration and Runtime Delivery
Shortcuts do not become active until the platform layer registers them with the host OS. This happens automatically when the app starts.
Configuring Shortcuts per Platform
During initialization, the runtime calls platform.services.configureShortcuts. Each platform implementation receives the slice of platform.Shortcut structs and registers them with native OS APIs.
For example, src/platform/macos/root.zig contains the macOS registration routine:
fn configureShortcuts(context: ?*anyopaque, shortcuts: []const platform_mod.Shortcut) anyerror!void {
// Platform-specific registration, e.g. NSMenu on macOS.
}
Source: src/platform/macos/root.zig lines 1766–1767【/cache/repos/github.com/vercel-labs/native/main/src/platform/macos/root.zig#L1766-L1767】. Equivalent configureShortcuts functions exist for Windows (src/platform/windows/root.zig) and Linux (src/platform/linux/root.zig).
How the Runtime Delivers Events
The full flow from declaration to handler follows four discrete steps:
- Manifest → AppInfo –
src/tooling/manifest.zigparses and validates shortcuts fromapp.zon. - Runtime → Platform –
platform.services.configureShortcutsregisters the validated shortcuts with the OS. - OS → Runtime – When the user presses a registered key combo, the OS sends a shortcut event back to the runtime.
- Runtime → App – If the shortcut maps to a command, the runtime invokes
Options.on_command. If the key is a chrome shortcut, the runtime invokesOptions.on_keyonly after the widget tree declines the event.
Complete Native SDK Shortcut Example
Here is a consolidated implementation showing manifest declarations, app scaffolding, and both callback types:
// 1️⃣ app.zon – declare shortcuts
shortcuts: [
{ id = "command.toggle", key = "t", modifiers = [ "command" ] },
{ id = "command.clear", key = "escape" },
]
// 2️⃣ UI app definition
pub fn MyApp(comptime Model: type, comptime Msg: type) type {
return UiAppWithFeatures(Model, Msg, .{});
}
// 3️⃣ Options wiring
const options = UiApp.Options{
.name = "Demo",
.scene = ...,
.canvas_label = "main",
.on_command = onCommand,
.on_key = onKey,
};
fn onCommand(name: []const u8) ?Msg {
return switch (name) {
"command.toggle" => Msg{ .Toggle = {} },
"command.clear" => Msg{ .Clear = {} },
else => null,
};
}
fn onKey(event: canvas.WidgetKeyboardEvent) ?Msg {
// Chrome shortcuts (space, play/pause) arrive here.
return switch (event.key) {
"space" => Msg{ .PlayPause = {} },
else => null,
};
}
Several example applications under examples/*/src/runner.zig demonstrate this pattern in practice. The calculator example, for instance, allocates a shortcuts buffer and uses a Chrome shortcut for Escape.
Summary
- Define shortcuts in
app.zonusing thenative_sdk.Shortcutschema declared insrc/platform/types.ziglines 82–99. - The manifest parser in
src/tooling/manifest.zigvalidates shortcuts viavalidateShortcutand stores them inAppInfo.shortcuts. - Standard keyboard shortcuts are handled by
UiApp.Options.on_commandand receive the manifestidas a[]const u8. - Chrome shortcuts use
UiApp.Options.on_keywith acanvas.WidgetKeyboardEventand only fire after the widget hierarchy declines the key. - Platforms register shortcuts via
configureShortcutsinsrc/platform/macos/root.zig,src/platform/windows/root.zig, andsrc/platform/linux/root.zig.
Frequently Asked Questions
What is the difference between keyboard shortcuts and chrome shortcuts in the Native SDK?
Keyboard shortcuts are global command shortcuts routed through on_command using the command id from the manifest. Chrome shortcuts are platform-specific window-level events, such as play/pause or back gestures, that bypass the widget hierarchy and are delivered through on_key only after the widget tree declines the event.
How do I declare modifier keys for a shortcut in app.zon?
You add the modifiers array to the shortcut entry in app.zon. For example, { id = "command.palette", key = "k", modifiers = [ "command" ] } registers cmd+K. The parser in src/tooling/manifest.zig validates the entry, and the ShortcutModifiers default in src/platform/types.zig ensures at least one modifier is present for character keys.
Why does my spacebar shortcut use on_key instead of on_command?
The Native SDK marks non-character keys like space as chrome shortcuts to prevent them from intercepting text input. Because these shortcuts must not steal focus from typing widgets, the runtime routes them through on_key after the widget tree has had a chance to consume the event.
Where does the OS registration happen at startup?
The runtime calls platform.services.configureShortcuts during initialization. On macOS this is implemented in src/platform/macos/root.zig around lines 1766–1767, with equivalent routines in src/platform/windows/root.zig and src/platform/linux/root.zig. Each platform translates the []const platform.Shortcut slice into native OS APIs such as NSMenu.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →