Skip to content

Widget options

Every Grist Custom Widget can persist a JSON object alongside the section configuration. This is how you store user preferences — view modes, filters, theme tweaks, custom column rules, AI prompts, etc.

Quick read from useGrist()

ts
const w = useGrist()

w.widgetOptions             // current value (Record<string, unknown> | null)
w.widgetInteraction         // { access_level?: string, ... } from Grist

await w.getWidgetOptions()  // explicit fetch
await w.getWidgetOption("theme")
await w.setWidgetOption("theme", "dark")
await w.setWidgetOptions({ theme: "dark", autoRefresh: true })
await w.patchWidgetOptions({ theme: "dark" })   // merge with current
await w.clearWidgetOptions()

widgetOptions updates reactively whenever Grist emits onOptions. You don't need to re-fetch after writing.

Opening the configuration panel

Set hasCustomOptions: true on the provider to show the gear icon in Grist's widget chrome. The SDK forwards onEditOptions so you can navigate to a settings route:

tsx
<GristWidgetProvider
  options={{
    hasCustomOptions: true,
    onEditOptions: () => navigate("/settings"),
  }}
>

Typed options with debounce

When using <GristWidgetProvider>, useGristWidgetOptionsFromContext gives you:

  • Typed defaults merged with whatever Grist has stored.
  • Debounced writes (default 250ms).
  • A namespace mode so you can share the options bag with unrelated state.
tsx
import { useGristWidgetOptionsFromContext } from "grist-widget-sdk"

type Settings = { theme: "light" | "dark"; rowsPerPage: number }

function SettingsPanel() {
  const { options, patchOptions, reset, loading } = useGristWidgetOptionsFromContext<Settings>({
    defaults: { theme: "light", rowsPerPage: 25 },
    debounceMs: 200,
    namespace: "ui", // optional — writes to grist.setOption("ui", value)
  })

  if (loading) return <Spinner />
  return (
    <>
      <select
        value={options.theme}
        onChange={(e) => patchOptions({ theme: e.target.value as Settings["theme"] })}
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <button onClick={reset}>Reset</button>
    </>
  )
}

Standalone alternative

If your widget doesn't use <GristWidgetProvider>, the advanced hook useGristWidgetOptions from grist-widget-sdk/advanced provides the same API but manages its own grist.ready() call internally. Don't mix the two — pick one per widget.

Storage model

Behind the scenes, Grist stores a single JSON object per widget. The SDK exposes two write modes:

APIEffect
setWidgetOption(key, value)grist.setOption(key, value) — replaces one key.
setWidgetOptions(next)grist.setOptions(next) — replaces the whole object.
patchWidgetOptions(patch)Merges patch into the current options and writes the merged result back.

When in doubt, prefer patchWidgetOptions — it's the closest analogue to React's "spread + override" pattern.

Interaction metadata

widgetInteraction is the second argument Grist sends to onOptions. It carries metadata about the widget's environment, notably access_level. You can use it to gate dangerous UI:

ts
const canWrite = w.widgetInteraction?.access_level === "full"

Pitfalls

  • Don't mix w.setWidgetOptions(...) with useGristWidgetOptionsFromContext in the same widget. Pick one writer or namespace the latter.
  • Don't write on every keystroke. Either debounce manually or use the advanced hook.
  • Don't store large data here. Widget options are sent with every onOptions event. Keep payloads under a few KB.

Released under the ISC License.