Grist Widgets

CLI reference

How create-grist-widget scaffolds a project, starts from a real example, and what the bundled GitHub Pages deploy pipeline does.

Getting started here covers the CLI's main commands and scaffolding a new project from a terminal, CLI-only. If you'd rather copy the template repo on GitHub, set up an AI coding agent, or connect a deployed widget to a real Grist document, see the Quickstart — both paths produce identical code.

npm create grist-widget

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

The template — and every starting example, and the logic that composes them — are embedded directly in the CLI. Nothing is fetched from GitHub or npm at scaffold time; the dependency install right after is the only network access any of this makes. Scaffolding via the CLI is the same starting point as grist-widget-template (what the Quickstart's template-copy path gives you), so both paths converge on identical code.

By default the scaffold is blank — a two-field editor — and its dependencies install automatically (the package manager is detected from how you invoked the command). Run npm create grist-widget with no name for an interactive prompt instead, which asks for both a name and a starting example.

What you get

index.html
main.tsx
App.tsx
App.test.tsx
eslint.config.js
.prettierrc
package.json
  • index.html has the grist-plugin-api.js script tag wired in.
  • src/main.tsx mounts the React root.
  • src/App.tsx shows the provider + boundary + useGrist() pattern, with placeholder UI you can immediately replace — or, with --example, a real starting widget instead of the placeholder.
  • src/App.test.tsx is a working test against the placeholder, using renderWithGrist — run it, then edit it alongside App.tsx.
  • Tailwind preconfigured for theme-aware widgets (light / dark / system).
  • Prettier + ESLint with project-wide rules.

After scaffolding, the first edit is typically src/App.tsx — replace the placeholder with the widget you want. The Cookbook has ten ready-to-paste recipes for common shapes, and create-grist-widget list shows every example the CLI itself can start you from directly (see below).

The four commands

CommandWhat it does
create <name> [--example <id>]Scaffold a new widget into ./<name> — same as npm create grist-widget <name>
init [--example <id>]Scaffold into the current directory instead of a new one
use <example-id>Swap the current project's widget for a different example
list [--json]Print the available starting examples

Common flags for create/init: --yes (skip interactive prompts), --no-install (skip the automatic dependency install), --pm <name> (force a specific package manager). init also takes --name <name> (default: the current directory's name) and --force (see below).

Starting from a real example

npm create grist-widget my-widget --example form-edit
# or interactively:
npm create grist-widget

create-grist-widget list prints every available id with a short description; list --json gives the same as machine-readable JSON. Every row below is exactly what list returns, generated from the same manifest the CLI itself embeds — the id is what --example <id> / use <id> take, and the same slug shown in its playground URL (/examples/<id>/latest/).

ExampleDescriptionUse
Attachment galleryThumbnail grid of every attachment in the selected row. Showcases extractGristAttachmentIdsFromCell + useGrist().fetchAttachmentBlob.attachment-gallery
Bulk mark doneBulk table.update with an array of row patches.bulk-mark-done
Declared columnsShows GRIST_OPTIONS column declarations and mappedRecord after the user maps columns.declared-columns
Row editorEdit the selected row in a polished form. Showcases column mappings, write status, and error UI.form-edit
hello-worldhello-world
Mark doneSingle-row write via table.update.mark-done
Reads demolistTables, fetchTableRows, fetchRow, fetchSelectedRecord.reads-demo
REST fetchfetchWithAuth against the Grist REST API.rest-fetch
Safe parsesafeParseGristTableData with per-cell issues.safe-parse
Schema migrationapplyActions with gristAddVisibleColumnAction, gristRenameColumnAction, and gristRemoveColumnAction.schema-migration
Schema previewuseGristSchema with replicaRowMode schema+samples for LLM context.schema-preview
Slice hooksuseGristStatus, useGristSelection, useGristWrites, useGristTheme in separate child components.slice-hooks
Spreadsheet gridSelected Grist section rendered as a fully-synced, editable spreadsheet via react-datasheet-grid, plus a schema toolbar and replica-document export.spreadsheet-grid
Task boardMini Kanban board grouped by Status. Drag a card to update Status; click to focus the row in linked sections.task-board
ThemeRead w.theme from the host document.theme-demo
Widget optionsPersist UI state with setWidgetOption / widgetOptions.widget-options
Table writescreate, update, upsert, destroy via w.table.writes-demo

init — scaffolding into an already-initialized directory

create refuses a non-empty target directory. init is the fix for the case that comes up constantly with cloud coding agents, which typically start from an already-cloned or already-git init'd repo rather than an empty one:

mkdir my-widget && cd my-widget
git init   # or: this is already a repo clone
npx create-grist-widget init --example form-edit

Files that already exist and are identical to the template are left untouched; the four identity-bearing files (package.json, README.md, index.html, src/App.tsx) get the usual name/title substitution even if present. Anything else that already exists and differs from the template's own copy stops the run and lists the conflicting paths — pass --force to overwrite anyway. If .git already exists, init never auto-commits; review and commit the result yourself. If it doesn't, init sets up main + dev and an initial commit exactly like create does.

use — swapping to a different example later

create-grist-widget use task-board

Swaps src/App.tsx (and any example-specific files) for a different example, in place. It only ever touches src/** — never main.tsx, vite.config.ts, or the deploy workflow — and it requires a clean git working tree first, so the swap is always a single reviewable, revertible diff. Commit or stash first if you have uncommitted changes; use refuses to run otherwise.

create, init, use, and list are themselves valid widget names — create-grist-widget list runs the list command, not "scaffold a widget named list." Use the explicit form (create-grist-widget create list) for a widget with one of these names.

The bundled deploy pipeline

Every scaffolded repo ships its own .github/workflows/deploy.yml, two channels from one workflow:

  • Push to main → the release channel: an immutable /<repo>/v<version>/ plus a mutable /<repo>/latest/ (and the same build at the repo's Pages root). Re-pushing main without bumping package.json's version is a no-op — it only rebuilds when a new v<version> doesn't exist yet.
  • Push to dev → the dev channel: a mutable /<repo>/dev/, for live review inside Grist as you iterate.

If you scaffolded via "Use this template" with Include all branches checked, GitHub Pages is already pointed at the copied gh-pages branch — nothing else to configure. Scaffolding a fresh, non-template repo yourself needs two one-time steps instead:

  1. Settings → Pages → Source: Deploy from a branch → gh-pages → / (the workflow creates the gh-pages branch itself, but Pages has to be pointed at it once — left on main, you'd see a blank page with a 404 for /src/main.tsx even though the workflow reports success).
  2. Settings → Actions → General → Workflow permissions → Read and write permissions.

Hand-rolled (no template)

If you want the bare minimum instead of the CLI:

mkdir my-widget && cd my-widget
pnpm init
pnpm add react react-dom grist-widget-sdk
pnpm add -D typescript @types/react @types/react-dom vite @vitejs/plugin-react

Then create index.html (with the plugin-api script tag), src/main.tsx (React root), and src/App.tsx (your widget) — see the Cheat sheet for the smallest possible code, or the Cookbook for ten ready-to-paste recipes. The template path saves about ten minutes of boilerplate and one or two configuration gotchas, but the SDK works fine without it.

On this page