Grist Widgets
SDKGuide

Getting started

Install the SDK, load the Grist plugin API, and get a minimal widget rendering inside a real Grist document.

The fastest path to a running Grist widget is the Vite template. The rest of this page covers manual install and the smallest possible widget for readers who want to see the parts.

TL;DR

npm create grist-widget my-widget
cd my-widget
pnpm install
pnpm dev

Open the printed URL — you see a placeholder telling you Grist is not available, because the dev server is running outside a Grist iframe.

To connect it to a real Grist document:

  1. Run pnpm build && pnpm preview (or deploy the dist/ folder to any static host — Cloudflare Pages, Netlify, GitHub Pages, etc.).
  2. In a Grist document, add a Custom Widget section, paste the public URL into the URL field, and press Save.
  3. The widget renders inside Grist with full plugin-api access.

You now have a running widget. See Raw plugin API vs SDK for why you use this package instead of calling grist directly. Skip ahead to the Cookbook for ten end-to-end recipes, the Cheat sheet for a one-page API reference, or Troubleshooting if something is misbehaving.

What the template gives you

The template scaffolded above ships with:

  • A Vite + React 19 + TypeScript app skeleton.
  • grist-widget-sdk and its peer dependencies pre-installed.
  • The host script tag for grist-plugin-api.js already wired in index.html.
  • A single-file App.tsx that demonstrates the recommended provider + boundary + useGrist() pattern.
  • Tailwind CSS preconfigured so the widget is theme-aware out of the box (light / dark / system).
  • A test setup using vitest + the SDK emulator.

See the CLI reference for what the scaffold contains and how its bundled deploy pipeline works.

Manual install

Skip this section if you used the template.

pnpm add grist-widget-sdk
# or
npm install grist-widget-sdk
# or
yarn add grist-widget-sdk

Peer dependencies:

react       >=18
react-dom   >=18

Load the Grist plugin API

The SDK expects the global grist object at runtime. Add the script in your app shell:

<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>

Hello world

hello-world in the playground is the canonical minimal widget. It is type-checked on every pnpm --filter playground build:widgets run (root pnpm test includes that step).

import { useGrist, type UseGristOptions } from "grist-widget-sdk"

export const GRIST_OPTIONS: UseGristOptions = {
  requiredAccess: "read table",
}

export function WidgetApp() {
  const w = useGrist()
  const rowKey =
    w.record && typeof w.record.id === "number" ? String(w.record.id) : w.mode

  if (w.mode === "empty") return <p>Select a row.</p>
  if (w.mode === "new-row") return <p>New row flow</p>
  return <p key={rowKey}>Selected row #{String(w.record!.id)}</p>
}

In your own Vite app, add GristWidgetProvider + GristBoundary around WidgetApp (the template does this in main.tsx; the playground does it in widget.html).

That's everything you need to:

  • Wait for Grist to finish its handshake.
  • Render a friendly fallback when the page is opened outside Grist.
  • Render an error UI if anything goes wrong, with a retry button.
  • Subscribe to the currently selected row.
  • Switch between empty / row / new-row modes.

What useGrist() returns

GroupProperties
Statusstatus, isAvailable, isReady, error, reload()
Selectionrecord, records, mappedRecord, mode, mappings, columnMappingStatus, isNewRecord
Writes (records)table, getTable(id), actionStatus, actionError
Writes (schema)applyActions(actions)
ReadsfetchTable, fetchTableRows, fetchRow, fetchSelectedTable, fetchSelectedRecord, listTables, getDocName
Widget optionswidgetOptions, getWidgetOptions, setWidgetOption, patchWidgetOptions, clearWidgetOptions
LinkingsetCursorPosition, setLinkedRowSelection
Attachments / RESTgetAttachmentUrl, fetchAttachmentBlob, uploadAttachment, getAccessToken, fetchWithAuth
Section APIconfigure, refreshMappings, currentTableId
Themetheme ("light" | "dark" | null)

See the API reference for every field.

Asking for write access and declared columns

declared-columns shows GRIST_OPTIONS.columns and columnMappingStatus — full source in the Cheat sheet under Mappings + column gate.

In the widget configuration panel, the user maps these logical names to real columns. You consume them via w.mappedRecord and w.mapBack(...) on writes — see Column mapping.

Run a write

mark-done is a single-row table.update; its source is in the Cheat sheet.

For bulk writes, pass an array and the SDK forwards it to Grist as one BulkUpdateRecord action — see bulk-mark-done in the Cookbook.

For schema changes, see schema-migration, also in the Cookbook.

Where to go next

  • Cookbook — ten end-to-end recipes for the most common widget shapes.
  • Cheat sheet — one-page API reference for daily use.
  • Troubleshooting — symptoms and fixes for the most common errors.
  • CLI reference — what npm create grist-widget scaffolds.
  • Core concepts — mental model behind selection modes, mappings, and the ready handshake.
  • TestingrenderWithGrist and the emulator.

On this page