Widget API

Dashboard widgets are the most common way plugins surface content in Ghosty. A widget occupies a bounded region on the user's home screen and can be resized between three size presets. The Widget API provides utilities for querying dimensions, responding to resize events, and matching the host application's theme.

Size presets

Ghosty widgets use a grid-based layout with three size presets:

PresetWidth (px)Height (px)Grid cells
small1601601 x 1
medium3401602 x 1
large3403402 x 2

The default size is declared in plugin.yaml under views.widget.default_size. Users can resize between the min_size and max_size boundaries set in the manifest.

Responsive design

Design your widget to look good at all three sizes. Use relative units and flexbox/grid layouts so content adapts gracefully when the user resizes.

Querying dimensions

Use ghosty.widget.getSize() to query the current size preset and pixel dimensions at any time.

GETghosty.widget.getSize()

Returns the current size preset and pixel dimensions of the widget container.

The returned object has this shape:

interface WidgetSize {
  /** Current size preset */
  preset: "small" | "medium" | "large";
  /** Container width in CSS pixels */
  width: number;
  /** Container height in CSS pixels */
  height: number;
}

Example

import { ghosty } from "@ghosty/sdk";
 
const size = ghosty.widget.getSize();
 
if (size.preset === "small") {
  renderCompactView();
} else {
  renderDetailedView();
}

Resize events

When the user resizes the widget, Ghosty fires a resize event. Subscribe to it with ghosty.widget.onResize().

POSTghosty.widget.onResize(callback)

Register a callback that fires whenever the widget is resized. Returns an unsubscribe function.

Parameters

NameTypeRequiredDescription
callback(size: WidgetSize) => voidYesCalled with the new size information after each resize.

Example

import { ghosty } from "@ghosty/sdk";
 
const unsubscribe = ghosty.widget.onResize((size) => {
  console.log(`Resized to ${size.preset}: ${size.width}x${size.height}`);
 
  // Re-render with the new dimensions
  if (size.preset === "small") {
    renderCompactView();
  } else if (size.preset === "medium") {
    renderMediumView();
  } else {
    renderFullView();
  }
});
 
// Later, when cleaning up:
// unsubscribe();
Debouncing

The resize callback may fire multiple times during an animated resize transition. If your re-render logic is expensive, debounce the callback to avoid jank.

Theme integration

Widgets should match the Ghosty host application's theme. The Widget API provides access to the current theme and fires events when the user toggles between light and dark mode.

GETghosty.widget.getTheme()

Returns the current theme configuration including mode and colour tokens.

The returned object:

interface WidgetTheme {
  /** Current mode */
  mode: "light" | "dark";
  /** Resolved colour tokens (CSS custom property values) */
  colors: {
    background: string;
    foreground: string;
    primary: string;
    primaryForeground: string;
    muted: string;
    mutedForeground: string;
    border: string;
    card: string;
    cardForeground: string;
  };
}
POSTghosty.widget.onThemeChange(callback)

Register a callback that fires when the host theme changes. Returns an unsubscribe function.

Parameters

NameTypeRequiredDescription
callback(theme: WidgetTheme) => voidYesCalled with the new theme after a theme change.

Theme example

import { ghosty } from "@ghosty/sdk";
 
function applyTheme(theme: WidgetTheme): void {
  const root = document.documentElement;
  root.style.setProperty("--bg", theme.colors.background);
  root.style.setProperty("--fg", theme.colors.foreground);
  root.style.setProperty("--primary", theme.colors.primary);
  root.classList.toggle("dark", theme.mode === "dark");
}
 
// Apply initial theme
applyTheme(ghosty.widget.getTheme());
 
// React to changes
ghosty.widget.onThemeChange(applyTheme);

Widget container

The widget's root DOM element is passed to your render function. This is a standard HTMLDivElement with the following guarantees:

  • It has overflow: hidden set by the host.
  • It is sized to match the current size preset.
  • It has position: relative so you can use absolute positioning inside it.
  • It has a transparent background by default. Use the theme colours to set your own background.

Render function signature

Your widget entry file must export a render function:

export function render(container: HTMLElement): void {
  container.innerHTML = `
    <div class="widget-content">
      <h3>My Widget</h3>
      <p>Content goes here.</p>
    </div>
  `;
}

You can use any rendering approach: raw DOM manipulation, a framework like Preact or Lit, or simple template strings.

Framework bundles

Keep widget bundle sizes small. The recommended maximum is 100 KB gzipped. Consider using lightweight alternatives like Preact (3 KB) instead of React (40 KB) for widget views.

Requesting fullscreen

If your plugin also declares a fullscreen view, you can transition to it from the widget with a single call:

import { ghosty } from "@ghosty/sdk";
 
document.getElementById("expand-btn")?.addEventListener("click", () => {
  ghosty.navigate.fullscreen();
});

This triggers the onHidden hook on the widget and the onVisible hook on the fullscreen view.

Auto-refresh

If refresh_interval is set in the manifest, the Ghosty runtime periodically calls a refresh function exported from your widget entry file:

export function refresh(container: HTMLElement): void {
  // Called every `refresh_interval` seconds
  // Re-fetch data and update the UI
  fetchLatestData().then((data) => {
    updateUI(container, data);
  });
}

The refresh function receives the same container element as render. It is only called while the widget is visible.

Manual refresh

Users can also manually trigger a refresh by long-pressing the widget and selecting "Refresh" from the context menu. This calls the same refresh function.

Best practices

  1. Design for all three sizes. Test your widget at small, medium, and large. Avoid fixed pixel dimensions.
  2. Use the theme colours. Never hard-code light or dark mode colours. Use the theme tokens so your widget adapts automatically.
  3. Keep the initial render fast. Show a skeleton or placeholder immediately and load data asynchronously.
  4. Clean up event listeners. Unsubscribe from resize and theme change events in your onDestroy hook.
  5. Minimise bundle size. Widgets are loaded on the user's home screen. Keep your JavaScript payload lean.
  6. Handle errors visually. If data loading fails, display a friendly error message inside the widget instead of leaving it blank.