Skip to main content

widgets

Render rich HTML inline via `<mcwidget>` tags with theme-aware styling. Load when emitting an mcwidget so the iframe inherits the dashboard theme instead of clashing with light / dark / custom palettes.

Source facts

Repository
kirodotdev/KiroCrew
Last source activity
October 24, 2026 at 02:06
Detected SKILL.md language
English
Stars
4,631
Forks
900

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
widgets
description
Render rich HTML inline via `<mcwidget>` tags with theme-aware styling. Load when emitting an mcwidget so the iframe inherits the dashboard theme instead of clashing with light / dark / custom palettes.
triggers
mcwidget, widget iframe, chart, table, visual, dashboard, render widget
# Inline widgets (`<mcwidget>`) You can embed rich HTML in assistant messages using `<mcwidget title="Title">HTML</mcwidget>`. The dashboard renders each tag as a sandboxed iframe with Tailwind CSS preloaded. Use this for styled visual content plain markdown cannot express: charts, color-coded tables, styled cards, visual summaries, simple interactive probes. Tailwind is preloaded and ready without any setup — it is served from the dashboard's own origin, not from `cdn.tailwindcss.com`, which the iframe CSP does NOT allow. Other libraries (Chart.js, D3, etc.) need a `<script src="…">` tag pulling from one of the CSP-allowed CDNs listed under Rules below. ## When to use - **Use** when styled HTML genuinely helps comprehension — Chart.js graphs, color-coded comparison tables, status cards, before/after previews, small interactive probes. - **Don't use** for content markdown already handles well — plain prose, bullet lists, fenced code, simple tables. Widget iframes cost more to render and add more context than markdown; default to markdown. - **Save to file** for anything large. `<mcwidget>` bodies fit comfortably up to a few KB; beyond that, write an HTML file and return the absolute path so the dashboard shows a thumbnail / link instead. ## Theme-aware styling (critical) The widget iframe inherits the dashboard's active theme through CSS custom properties injected into each srcdoc. Use these variables instead of hardcoded colors so widgets render correctly on every theme (light, dark, and user-defined custom palettes). Core palette (use these first): | Variable | Role | |-----------------|---------------------------------| | `var(--bg)` | Page background | | `var(--text)` | Foreground text | | `var(--card)` | Card / panel background | | `var(--card-fg)`| Foreground text on `--card` | | `var(--border)` | Borders and dividers | | `var(--accent)` | Primary accent / links | | `var(--muted)` | Muted / secondary text | | `var(--ok)` | Success state | | `var(--warn)` | Warning state | | `var(--danger)` | Error / destructive state | | `var(--info)` | Info / neutral callout (blue) | Extended palette (also available): | Variable | Role | |-------------------------|-----------------------------------------------------| | `var(--bg-elevated)` | Raised surface above `--bg` (headers, modals) | | `var(--bg-hover)` | Hover state for interactive backgrounds | | `var(--text-strong)` | High-contrast text emphasis | | `var(--muted-strong)` | Stronger muted text (still secondary, more legible) | | `var(--border-strong)` | High-contrast border for emphasis | | `var(--accent-hover)` | Hover state for accent elements | | `var(--accent-subtle)` | Tinted background using accent (badges, highlights) | | `var(--ok-subtle)` | Tinted ok background (success banners) | | `var(--warn-subtle)` | Tinted warn background (warning banners) | | `var(--danger-subtle)` | Tinted danger background (error banners) | With Tailwind, use arbitrary values: `bg-[var(--card)]`, `text-[var(--card-fg)]`, `border-[var(--border)]`. Avoid `bg-gray-900`, `text-white`, `bg-white`, etc — they clash the moment the user switches themes. Only the names in these two tables reach the sandboxed frame; a parent theme override cannot add more. Two common names are aliased for you: `--surface` resolves to `--card` and `--surface-secondary` to `--bg-elevated`. Any other name (`--surface-raised`, `--panel`, ...) is undefined inside the frame, so `var(--panel, #1e1e1e)` always paints its dark literal and is unreadable on a light theme. ### The theme contract These rules apply to **every artifact-bound HTML document**, not just inline widgets — a page generated by a script and pushed via `artifact_save` / `artifact_update` or the `kirocrew artifact` CLI renders in the same themed iframe: - **Never set one half of a foreground/background pair.** The iframe injects `body{background:var(--bg);color:var(--text)}` defaults, so a lone hardcoded `color:#111` lands on a dark canvas in dark mode (and a lone light `background` swallows themed light text). Set both halves, or neither. - **Hardcode nothing; fall back instead.** Content that must also render standalone (outside the dashboard, where no theme vars exist) keeps a literal fallback inside the var: `color:var(--text,#111); background:var(--bg,#fff)`. Inside the dashboard the theme wins; standalone, the fallback reproduces the intended palette. - **The renderer has a last-resort fallback, not a licence.** When a widget carries a hardcoded light background (`bg-white`, `bg-<hue>-50|100|200`, a light `#hex`) and references no `var(--…)` at all, a dark dashboard renders it on a neutral light canvas with dark text instead of white-on-white. That is a readability floor for content that already slipped through: it cannot honor the user's palette, it turns off the moment a single theme var appears, and it does nothing for a dark hardcoded palette. Write to the contract above; never design for the fallback. - `artifact_save` / `artifact_update` attach a `⚠️` hint when widget/html content carries literal colors (`#hex` / `rgb()` / `hsl()`) and no `var(--…)` reference. Treat that hint as a defect to fix in the same turn — **unless the user explicitly asked for a fixed palette** (brand colors, a print-faithful mock): the heuristic cannot distinguish deliberate branding, so honor the user's choice and say the hint was intentionally not applied. ## Format ```html <mcwidget title="Deploy status"> <div class="p-4 rounded-lg" style="background:var(--card);color:var(--text);border:1px solid var(--border)"> <div class="text-sm font-semibold" style="color:var(--ok)">✅ Green across all stages</div> <div class="text-xs" style="color:var(--muted)">last build 08:42 UTC</div> <div class="text-xs"> <a href="https://github.com/example-org/example-repo/pull/1234" target="_blank" rel="noopener noreferrer" style="color:var(--accent)">#1234</a> </div> </div> </mcwidget> ``` The opening tag takes two attributes: `title=` and `slug=`. A `slug=` binds the impression to an artifact that already exists and suppresses registration; omit it for new content. Put `<mcwidget>` on its own line after a blank line, never glued to a closing ``` fence. ## Every widget is already an artifact Each finalized `<mcwidget>` without `slug=` is auto-registered on the backend, keyed idempotently by message timestamp and a fingerprint of the widget body (two identical bodies in one message are one artifact), even if never viewed. Incognito and temporary sessions never register widgets. Unpinned auto-registered widgets are pruned oldest-first past 200; the user's star keeps one out of the sweep. Do **not** call `artifact_save` on emitted content: it creates a duplicate and returns a warning. `slug=` only binds an impression; it skips registration and neither reads nor writes the artifact. To revise one, call `artifact_get`, then `artifact_update` to persist a new version, and re-emit with `<mcwidget title="…" slug="<known-slug>">`. Rules: - One `<mcwidget>` per visual payload — don't nest widgets. - Keep the body self-contained: Tailwind classes, inline `style=`, or a short `<style>` block are all fine. Inline `<script>` is permitted for small visualization code; third-party `<script src="…">` must target one of the CSP-allowed CDNs (`cdn.jsdelivr.net`, `cdnjs.cloudflare.com`). - For Chart.js, pull the library from jsdelivr and instantiate against a `<canvas>` inside the widget body, e.g.: ```html <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <canvas id="c"></canvas> <script>new Chart(document.getElementById('c'), { /* config */ })</script> ``` - **Give a responsive canvas a definite height, or leave the aspect ratio alone.** The Chart.js defaults (`responsive: true`, `maintainAspectRatio: true`) are safe exactly as written above. Only set `maintainAspectRatio: false` when the canvas's parent has an explicit pixel height: ```html <div style="height:180px"><canvas id="c"></canvas></div> ``` Without one, the canvas and its parent size each other and the self-sizing iframe re-feeds the loop, so the chart never settles and the widget renders blank. Any library that sizes a canvas from its container — Chart.js, ECharts, Plotly's responsive mode — carries the same hazard; inline SVG with a fixed `viewBox` has no feedback path at all. - The iframe CSP is `default-src 'none'`, which forbids more than script sources: there are **no network calls** (`connect-src 'none'`, so `fetch`, XHR and websockets fail), **no remote images or web fonts** (`img-src data: blob:`, `font-src data:` — use inline SVG or a data URI), **no form submission** (`form-action 'none'` — use the `data-action` event path below), and **no `eval`** (`'unsafe-eval'` is not granted, so a library that compiles at runtime is dead on arrival). Pass every value the widget needs in its HTML; a widget cannot fetch its own data. A silently blank widget is usually one of these. - Your widget's own CSS is NOT sanitized: the CSP above is the only control on it. `website/src/lib/cssSanitize.ts` filters only the theme variable VALUES the dashboard injects into the frame, so do not rely on it to strip anything you write. ## Links **Always open links in a new tab.** Every `<a>` inside an `<mcwidget>` MUST carry `target="_blank"` AND `rel="noopener noreferrer"`. The widget iframe is sandboxed; without `target="_blank"`, navigation either fails silently or replaces the iframe content with the link target — both broken UX. The `rel` attribute is non-negotiable for security: it blocks reverse-tabnabbing and prevents the destination from accessing `window.opener`. Style links with `style="color:var(--accent)"` (or Tailwind `text-[var(--accent)]`) for theme-correct contrast. Add `hover:underline` or `underline` based on density — long copy benefits from underline, chips / badges look cleaner without. ```html <a href="https://github.com/example-org/example-repo/pull/1234" target="_blank" rel="noopener noreferrer" style="color:var(--accent)">#1234</a> ``` **Render identifiers and bare URLs as links** to their known canonical `https://` targets, keeping the identifier as the label. If a canonical URL is unknown, leave the identifier as plain text rather than guessing. For chat messages, paste the full URL the user shares; never reconstruct. When the visible label can be made shorter than the URL (e.g. a long doc title), use a meaningful label (`<a href="…">Migration design doc</a>`) rather than dumping the raw URL. ## Animation Move only when the motion itself carries information. The test: freeze one frame. If the meaning survives, keep it still. | Content | Animate? | Example | |---|---|---| | Change over time | Yes | a CI queue growing | | Steps in order | Yes | one request walking the stack | | Things flowing | Yes | messages entering a queue | | Before / after | Yes | old vs new layout | | Space, 3D | Yes | an architecture diagram turning | | The user asked for animation | Yes | a playful scene | | Lookups, tables | No | a PR list | | Code, prose | No | a diff, a report | | Something watched all day | No | a status panel | Every animation: - Has a play / pause control, unless it runs under ~5 seconds and stops. - Honours `matchMedia('(prefers-reduced-motion: reduce)')`: show an equal still version instead, such as every step labelled side by side. - Stops when it is done. Only small decorative loops may run forever. - On Canvas, uses `requestAnimationFrame` and sizes the canvas to `devicePixelRatio`, so it stays sharp and pauses in a hidden tab. - Carries every value it needs in the HTML: the frame blocks runtime fetches (see Rules). How to move is the `living-ui` skill's job. Once the test above says move, load it: it covers how motion feels, where the eye goes, where words sit, keyboard and screen-reader use, and how to check the result before you show it. It never picks a colour, a font or a shape. If the user wants to take it elsewhere (a slide, Slack, an email), a widget is the wrong container: render a video or GIF file instead. ## Interactive widgets Widgets can send events back to the agent. Add `data-action` and an optional `data-payload` (JSON string) attribute to any clickable element: ```html <button data-action="approve" data-payload='{"id":"123"}'>Approve</button> ``` When clicked, the dashboard PRE-FILLS the chat composer with a message of the form `[UI] approve: {"id":"123"}`; it never sends it on its own. The user reviews the text and presses send, and only then does the agent receive it as a normal message and respond with text, a new widget, or both. Do not tell the user a click "submits" anything, and do not wait on a click as if it were a reply: nothing arrives until they send. Form inputs (`<input>`, `<select>`, `<textarea>`) carrying a `name` (or `id`, or `data-field`) are auto-collected on click and merged into the payload as `formData`. Use this for creation forms: render pre-filled elements, the user adjusts values, clicks the action button, then sends the pre-filled message, and the agent receives every field. Styling for interactive controls (consistent with the dashboard chrome): - Buttons: `text-xs py-1.5 px-3.5 rounded-md` + theme-var background. - Labels: `text-[11px]` + `text-[var(--muted)]`. - Inputs: `text-sm px-2.5 py-2 rounded-md` + `bg-[var(--bg)]` + `border-[var(--border)]`. - Zero hardcoded hex colors. Use the theme variables above for every background, foreground, and border. ## Density config `cfg.dashboard.widget_density` (`more` / `less`, default `more`) controls the wording of the short pointer that lives in the system prompt. `less` biases against widgets by default; `more` encourages them. Users can flip this in dashboard settings. Either way, this skill holds the full rules — the main prompt only discovers that `<mcwidget>` exists.
View on GitHub