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:100vwormin-width:1024px. - Target height ~360–480 px. Prefer a denser form over scrollbars.
- One focus per call; unrelated visuals → multiple
show_widgetcalls.
Hard rules for widget_code
- HTML fragment only — inject into
<div id="root">. No<!DOCTYPE>,<html>,<head>, or<body>. - Stream order: short
<style>→ visible HTML →<script>last. Scripts run only after streaming finishes. - No network from the widget: fetch / XHR / WebSocket blocked. Embed all
data. Loading libraries from allowed CDNs is fine (
script-src, notconnect-src). - No
localStorage/sessionStorage(they throw). Use in-memory vars. - CDN allowlist only:
cdnjs.cloudflare.com,cdn.jsdelivr.net,unpkg.com,esm.sh. - Total
widget_codeunder ~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:
jsconst 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.


