AI agent skill · by @cubeplexai
Show Widget
Build inline interactive HTML widgets with show_widget — charts, diagrams, dashboards, sliders, side-by-side comparisons, or any visual explainer. Load before non-trivial widget work for design rules and scenario skeletons.
Archived 2026-10-23 · Source updated 2026-10-10. Check the original repository for the latest version.
SKILL.md instructions
# show-widget — inline interactive HTML
Render with the **`show_widget`** tool. Stream `widget_code` as an HTML
fragment; do **not** `write` + `save_artifact` first — the widget is the
answer.
## When to use
Prefer `show_widget` when the user is better off *seeing* than reading:
画 / 绘制 / 可视化 / 示意图 / 流程图 / 架构图 / 图表 / 仪表盘 / 对比 /
draw / visualize / chart / diagram / dashboard / flowchart / architecture /
mockup / sliders / animation / explainer / side-by-side / breakdown.
Skip for pure text, code blocks, short facts, or errors.
## Size
This is a widget, not a webpage:
- Target ~600 px wide (max 640). Host iframe is centered/capped — no
`width:100vw` or `min-width:1024px`.
- Target height ~360–480 px. Prefer a denser form over scrollbars.
- One focus per call; unrelated visuals → multiple `show_widget` calls.
## Hard rules for `widget_code`
1. **HTML fragment** only — inject into `<div id="root">`. No `<!DOCTYPE>`,
`<html>`, `<head>`, or `<body>`.
2. **Stream order:** short `<style>` → visible HTML → `<script>` last.
Scripts run only after streaming finishes.
3. **No network from the widget:** fetch / XHR / WebSocket blocked. Embed all
data. Loading libraries from allowed CDNs is fine (`script-src`, not
`connect-src`).
4. **No `localStorage` / `sessionStorage`** (they throw). Use in-memory vars.
5. **CDN allowlist only:** `cdnjs.cloudflare.com`, `cdn.jsdelivr.net`,
`unpkg.com`, `esm.sh`.
6. Total `widget_code` under ~256 KB.
## Visual quality
Host defines these CSS variables on `:root` (light/dark):
| Token | Use |
| --- | --- |
| `--bg` | page background |
| `--fg` | text |
| `--muted` | card / panel surface |
| `--border` | separators |
| `--accent` | primary / links |
Always use `var(--bg)` etc. **Never hard-code colors** (`#…`, `rgb()`, named
colors) — widgets must work in both themes. Two font weights max (400, 500).
No gradients, drop shadows, or blur. Radii 4–8 px. system-ui is already set.
### Theme reactivity (Canvas / Chart.js / D3)
Anything that **reads** CSS vars into JS at render time freezes those colors.
Observe `:root` `style` changes and re-apply tokens:
```js
const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
function tokens() {
return { fg: css('--fg'), acc: css('--accent'), bd: css('--border') };
}
new MutationObserver(() => { /* re-read tokens + chart.update('none') */ })
.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
```
Inline SVG/HTML using `var(--fg)` directly does **not** need this — cascade
re-evaluates automatically.
## Chart.js height (mandatory)
With `responsive:true` and `maintainAspectRatio:false`, Chart.js reads the
canvas **CSS** height — not the HTML `height` attribute. Without an explicit
CSS height pinned with `!important`, the canvas grows unbounded and freezes
the tab.
- One chart: `canvas{width:100%!important;height:300px!important;}`
- Multiple heights: per-id CSS or inline `style="width:100%;height:180px;display:block;"`
## Scenario skeletons
Pick the closest and adapt — swap data/labels; do not echo verbatim.
### A. Single chart (Chart.js)
```html
<style>
.card{background:var(--muted);border:1px solid var(--border);border-radius:8px;padding:16px;}
.card h3{margin:0 0 8px;font-size:14px;font-weight:500;color:var(--fg);}
canvas{width:100%!important;height:300px!important;}
</style>
<div class="card">
<h3>Title that says what is plotted</h3>
<canvas id="c"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script>
const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
function tokens(){ return { fg: css('--fg'), acc: css('--accent'), bd: css('--border') }; }
let t = tokens();
const chart = new Chart(document.getElementById('c'), {
type: 'line',
data: {
labels: ['A','B','C','D','E'],
datasets: [{
label: 'Series', data: [3,8,5,12,9],
borderColor: t.acc, backgroundColor: t.acc+'33', tension: .3
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: t.fg } } },
scales: {
x: { ticks: { color: t.fg }, grid: { color: t.bd } },
y: { ticks: { color: t.fg }, grid: { color: t.bd } }
}
}
});
new MutationObserver(() => {
t = tokens();
chart.options.plugins.legend.labels.color = t.fg;
chart.options.scales.x.ticks.color = t.fg;
chart.options.scales.x.grid.color = t.bd;
chart.options.scales.y.ticks.color = t.fg;
chart.options.scales.y.grid.color = t.bd;
chart.data.datasets[0].borderColor = t.acc;
chart.data.datasets[0].backgroundColor = t.acc + '33';
chart.update('none');
}).observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
</script>
```
### B. Architecture / flow (inline SVG)
```html
<style>
.diag{display:block;width:100%;height:auto;}
.node{fill:var(--muted);stroke:var(--border);}
.label{fill:var(--fg);font:500 12px system-ui;text-anchor:middle;dominant-baseline:central;}
.edge{stroke:var(--border);stroke-width:1.5;fill:none;}
.edge.hl{stroke:var(--accent);}
</style>
<svg class="diag" viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0 0 L10 5 L0 10 z" fill="context-stroke"/>
</marker>
</defs>
<rect class="node" x="20" y="80" width="120" height="60" rx="6"/>
<text class="label" x="80" y="110">Client</text>
<rect class="node" x="220" y="80" width="120" height="60" rx="6"/>
<text class="label" x="280" y="110">API</text>
<rect class="node" x="420" y="80" width="120" height="60" rx="6"/>
<text class="label" x="480" y="110">DB</text>
<line class="edge" x1="140" y1="110" x2="220" y2="110" marker-end="url(#a)"/>
<line class="edge hl" x1="340" y1="110" x2="420" y2="110" marker-end="url(#a)"/>
</svg>
```
One shared marker with `fill="context-stroke"` so each arrowhead inherits its
edge stroke. Never hard-code marker fill. Use `markerUnits="userSpaceOnUse"`
for constant head size.
### C. Dashboard (metrics + small chart)
```html
<style>
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:12px;}
.stat{background:var(--muted);border:1px solid var(--border);border-radius:8px;padding:12px;}
.stat .v{font-size:22px;font-weight:500;color:var(--fg);}
.stat .l{font-size:11px;color:var(--fg);opacity:.7;margin-top:2px;}
.panel{background:var(--muted);border:1px solid var(--border);border-radius:8px;padding:16px;}
canvas{width:100%!important;height:200px!important;}
</style>
<div class="grid">
<div class="stat"><div class="v">1,284</div><div class="l">Active users</div></div>
<div class="stat"><div class="v">$42.1k</div><div class="l">Revenue (MTD)</div></div>
<div class="stat"><div class="v">98.6%</div><div class="l">Uptime</div></div>
</div>
<div class="panel"><canvas id="c"></canvas></div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script>/* Chart.js init — same token + MutationObserver pattern as A */</script>
```
### D. Interactive explainer (sliders)
```html
<style>
.row{display:flex;align-items:center;gap:12px;margin:8px 0;}
.row label{flex:0 0 110px;color:var(--fg);font-size:13px;}
.row input{flex:1;accent-color:var(--accent);}
.row .val{flex:0 0 64px;text-align:right;font-variant-numeric:tabular-nums;color:var(--fg);}
.out{margin-top:12px;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--muted);}
.out .big{font-size:24px;font-weight:500;color:var(--fg);}
.out .lbl{font-size:11px;color:var(--fg);opacity:.7;}
</style>
<div class="row"><label>Principal</label><input id="p" type="range" min="1000" max="100000" step="100" value="10000"><div class="val" id="pv">$10,000</div></div>
<div class="row"><label>Years</label><input id="y" type="range" min="1" max="40" step="1" value="10"><div class="val" id="yv">10</div></div>
<div class="row"><label>Rate (%)</label><input id="r" type="range" min="0" max="15" step="0.1" value="5"><div class="val" id="rv">5%</div></div>
<div class="out"><div class="big" id="ans">$16,288.95</div><div class="lbl">Future value</div></div>
<script>
const $ = (id) => document.getElementById(id);
function recalc() {
const P = +$('p').value, y = +$('y').value, r = +$('r').value / 100;
$('pv').textContent = '$' + P.toLocaleString();
$('yv').textContent = y;
$('rv').textContent = (r * 100) + '%';
$('ans').textContent = '$' + (P * Math.pow(1 + r, y)).toLocaleString(undefined, { maximumFractionDigits: 2 });
}
['p', 'y', 'r'].forEach((id) => $(id).addEventListener('input', recalc));
</script>
```
### E. Side-by-side comparison
```html
<style>
table{width:100%;border-collapse:collapse;font-size:13px;color:var(--fg);}
th,td{padding:8px 10px;border-bottom:1px solid var(--border);text-align:left;}
th{font-weight:500;color:var(--fg);opacity:.85;background:var(--muted);}
.check{color:var(--accent);font-weight:500;}
.dash{opacity:.5;}
</style>
<table>
<thead><tr><th>Feature</th><th>Option A</th><th>Option B</th></tr></thead>
<tbody>
<tr><td>Streams</td><td class="check">Yes</td><td class="dash">–</td></tr>
<tr><td>Offline</td><td class="dash">–</td><td class="check">Yes</td></tr>
<tr><td>Free tier</td><td>1k req/mo</td><td>500 req/mo</td></tr>
</tbody>
</table>
```
If nothing fits: bordered muted cards, accent sparingly, small radii, no
shadows. A plain card beats a sprawling page.