googlicius/obsidian-steward/stateful-widget
Claude Code and agent skill by googlicius/obsidian-steward
- community
Checking that the SKILL.md file is still in the repository.
Build interactive HTML project widgets with persisted runtime state (games, counters, forms). Read before show_widget when user actions must survive reopening the note.
Install
npx vskill@latest install googlicius/obsidian-steward/stateful-widgetSKILL.md
Stateful Widget Skill
Use this skill when creating interactive HTML project widgets (games, counters, quizzes, forms) where user actions must persist after the user closes and reopens the conversation note.
When to use
- User asks for a game, interactive demo, counter, quiz, or any widget that changes based on clicks/keyboard input.
- You use
show_widgetwith project mode (files: [{ name, content }, ...]), not a singlecodeblob. - State must survive: reopening the note, scrolling away and back, or editing widget code files (hot-reload).
Skip this pattern for static animations, one-shot diagrams, or SVG-only widgets with no runtime state.
How a widget is rendered
- Create (`show_widget`): Writes project files under
{stewardFolder}/Widgets/{widgetId}/(index.html,main.js,style.css,Widget.md, etc.).widgetId= slugifiedwidgetName+ short unique suffix. Appends astw-widget-projectfence to the conversation note:
``stw-widget-project <widgetId> ``
- Mount (`WidgetPostProcessor`): When the note is displayed, the post-processor reads the fence, resolves
{stewardFolder}/Widgets/{widgetId}, bundles the entry HTML (inlines linked CSS/JS from the project folder), injectswindow.stwfor state and assets, and renders the bundle in a sandboxed iframe (srcdoc,allow-scripts, strict CSP, no network). Manifestassetsare not inlined into HTML; the iframe requests allowed files from the host at runtime and creates local blob URLs.
- Hot-reload: Edits to project files via
edittrigger a re-bundle and iframe refresh.
Only project (HTML multi-file) widgets get the state bridge and state.json. Single-blob code widgets do not persist runtime state this way.
How state is managed
| Piece | Role | |
|---|---|---|
window.stw | API injected into the iframe by the host (not written by you in vault files). | |
window.stw.getState() | Returns last saved data object, or null on first load. | |
window.stw.setState(data) | Saves a JSON-serializable snapshot; debounced ~400ms, then written to vault. | |
state.json | Created lazily in the project folder on first successful setState. Host-owned; do not author or edit it manually. |
Note: You must call setState after every meaningful state change. Without it, state.json never appears.
On-disk shape (host-managed)
Your widget only supplies the inner data object via setState. Define a schema that fully describes the UI (e.g. board cells, score, turn).
Manifest assets and window.stw.getAsset
Only vault paths listed in the manifest assets array may be loaded. The host injects an allowlist registry on window.stw.assets (vault paths, not loadable URLs). Keys:
| Key | Example manifest path | Use in state | |
|---|---|---|---|
| Stem (filename without extension, when unique) | Images/x.png | "x" | |
| Vault path (normalized) | Images/x.png | "Images/x.png" — always safe; required when stems collide |
window.stw.getAsset(id) is async (Promise<string | null>). The iframe asks the host for file bytes, then creates a local blob URL inside the iframe. Results are cached per id for the session.
Add a new image by editing only Widget.md manifest assets (then save). Hot-reload rebuilds the registry — no main.js map update if you already look up by stem.
Static markup: asset:… in HTML attributes (src, href, poster) and CSS url(asset:…) is auto-hydrated on load. You can still author:
Dynamic UI: store a short id in state (stem or vault path) and resolve at render time:
Declare every vault path in manifest assets. Use unique filenames when relying on stem keys; if two assets share a stem, use the full vault path in state and in getAsset.
Do not persist asset paths or blob URLs in state
state.json is not processed for assets at load time.
- Do not put
asset:…paths, vault paths, or blob URLs insetStatedata. - Do store a short id (filename stem when unique, otherwise full vault path) and resolve via
await window.stw.getAsset(id).
Splitting into multiple files
When a file grows large — or you or the user want smaller parts — add extra files in the project root (no subfolders). Link every CSS/JS from index.html in dependency order (dependencies first, main.js last). The bundler inlines linked files at render.
- Classic scripts only — shared globals or one
windownamespace; noimport/export. - Keep
main.jslast: hydrate,setState, initialrender, andregisterActionif used.
Required JavaScript pattern (main.js)
Put hydrate + save in main.js (or inline script in index.html if you do not split files). Scripts run after window.stw is injected in the document head. If render uses assets, make it async and await getAsset.
Rules:
getState()once at startup (or merge with defaults).setState(state)after every change users should see after reopening.- Keep
datasmall and JSON-safe (no functions, DOM nodes, or circular refs). - Never store
asset:paths, vault paths, or blob URLs in state — persist stem ids (or full vault paths) and useawait stw.getAsset(see above). - Use one object as source of truth; re-render from it.
Widget.md — name: manifest block (host-maintained)
show_widget creates {projectPath}/Widget.md with a single yaml fence at the top of the note body. The host writes it on create — do not remove or replace the block. You may edit it later to add or update assets and maxAssetSize (e.g. for large audio). Any other fences (actions, actors, agent) go below the manifest fence (see interactive-widget).
| Field | Type | Required | Description | |
|---|---|---|---|---|
name | manifest | Yes | Block type literal. | |
entry | string | Yes | Relative path to the HTML entry file (e.g. index.html). | |
type | html | Yes | Must be html for project widgets. | |
widgetId | string | No | Project folder id; host sets on create. | |
widgetName | string | No | Display name from show_widget. | |
assets | array of strings | No | Allowlist of vault paths the iframe may request. Exposed on window.stw.assets; resolve with await getAsset(stem) or await getAsset("Path/to/file"). Reference in static HTML/CSS/JS as asset:Path/to/file. Only listed paths are served. | |
maxAssetSize | string or number | No | Per-file byte cap when the host reads an asset. Plain numbers are bytes (e.g. 5000000). Suffixes supported: B, KB, MB, GB (spacing optional, e.g. 5MB, 5 mb, 5 MB). Host sets 5MB on create; default when omitted from manifest: 5MB. Raise this for larger assets. |
Example (host-written on create):
Large assets: if an asset exceeds maxAssetSize, the host refuses the request (asset_too_large, logged in the developer console). If a path is missing from the vault, you will see asset_not_found. Use edit on Widget.md to add paths to assets or increase maxAssetSize when media fails to load.
Project layout checklist
index.html— entry; list all<link>/<script src>in dependency order.main.js— bootstrap: hydrate/save + initial render (last script).- Other
.js/.css— optional splits when files get large (project root only). Widget.md— host-created; manifest fence at top (see above). Do not edit the manifest block.state.json— auto-created on firstsetState; do not include inshow_widgetfiles.
Notes
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingstandard-skills/widget/stateful-widget/SKILL.md
Popularity
- Repository stars
- 85
- Repository forks
- 2
- 7-day trend score
- 22
Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 7 Nov 2026, yesterday.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/googlicius/obsidian-steward/stateful-widget)