Pane types / html
html
Last resort. Reach for html only when no structured type can carry the content — a bespoke visual, an unusual layout, a table that isn't a list. A structured pane inherits the design system for free; an html pane is on its own, and a mediocre one looks foreign in a feed of well-made cards.
A fully custom, purely visual pane: arbitrary HTML plus optional CSS, intended to fill the device's available space. JavaScript is not supported — scripts in the markup do not execute on the client.
There’s no reference render of this type yet. The examples below show what to send.
Fields in data
The envelope around data (publisher, label, expiresAt…) is the same on every type: see the envelope.
| Field | Type | Notes | |
|---|---|---|---|
variant | "v1" | optional | Design variant to render (defaults to v1). |
html | string | required | 1–200000 chars |
css | string | optional | up to 100000 chars |
responsive | boolean | optional | default true |
Craft
- Match the design system or the card will look like an ad. Daylight palette: paper #F4EEE2, card #FFFFFF, ink #15130F, secondary #5C544A, accent #FF5A1F, positive #1F7A3A, negative #C8341A, hairlines ink at 10% opacity.
- Use system fonts (
-apple-system, system-ui, sans-serif) — web fonts won't load. Match the app's rhythm: one display-size number or heading, everything else small. - Size everything in relative units (%, rem, ch, fr, clamp()). Fixed pixel widths break on narrow phones — the pane fills the device's available space.
- Handle both themes: default to light, then override under
@media (prefers-color-scheme: dark). A white card on a dark feed is glaring. - Style with the
cssfield, not inlinestyle=attributes — it's the field the renderer places in a <style> block, and it keeps the markup readable. - Assets must be public HTTPS URLs or inlined
data:URIs. Prefer inline SVG for anything graphical: it scales, needs no network, and inherits color. - Keep it to one screen.
htmlpanes get the device's available space, not a scroll view. - No JavaScript, ever:
<script>andon*handlers do not execute. Everything must be static. If you need interaction, model it as a structured pane type instead. - Set
responsive: falseonly when the content has a fixed natural size (a small diagram); the defaulttrueis almost always right.
Avoid
- Don't rebuild a card a structured type already gives you — an
htmlKPI is strictly worse than akpi. - Don't ship a full page: no <html>/<head>/<body> wrapper, no CSS reset, no framework classes (Tailwind/Bootstrap aren't loaded).
- Don't rely on JavaScript for layout or content — anything script-generated renders empty.
- Don't use fixed
pxwidths, absolute positioning, or viewport units that assume a desktop canvas. - Don't put walls of text in an
htmlpane because it doesn't fit elsewhere — split it into several structured panes.
Examples
Complete payloads for POST /api/v1/feed/items or the MCP publish_pane tool.
A comparison table — genuinely tabular, so no structured type fits
{
"type": "html",
"publisher": "Cost Watch",
"publisherInit": "CW",
"publisherColor": "#FF5A1F",
"label": "Cloud spend · July",
"data": {
"html": "<div class=\"wrap\">\n <p class=\"kicker\">Month to date</p>\n <p class=\"total\">$8,412</p>\n <table>\n <tr><td>Compute</td><td class=\"n\">$4,180</td><td class=\"up\">+12%</td></tr>\n <tr><td>Storage</td><td class=\"n\">$2,640</td><td class=\"flat\">±0%</td></tr>\n <tr><td>Egress</td><td class=\"n\">$1,592</td><td class=\"down\">−8%</td></tr>\n </table>\n</div>",
"css": ".wrap { font-family: -apple-system, system-ui, sans-serif; color: #15130F; padding: 1.25rem; }\n.kicker { margin: 0; font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: #9A8F7E; }\n.total { margin: .25rem 0 1rem; font-size: clamp(2rem, 12vw, 3rem); font-weight: 700; letter-spacing: -.02em; }\ntable { width: 100%; border-collapse: collapse; font-size: .9rem; }\ntd { padding: .5rem 0; border-bottom: 1px solid rgba(21,19,15,.1); }\ntd.n { text-align: right; font-variant-numeric: tabular-nums; }\ntd.up, td.down, td.flat { text-align: right; width: 4ch; font-size: .8rem; }\n.up { color: #C8341A; } .down { color: #1F7A3A; } .flat { color: #9A8F7E; }\n@media (prefers-color-scheme: dark) {\n .wrap { color: #F4EEE2; }\n td { border-bottom-color: rgba(244,238,226,.14); }\n}",
"responsive": true
}
}A small custom visual, drawn as inline SVG so it scales and needs no network
{
"type": "html",
"publisher": "Habit",
"publisherInit": "H",
"publisherColor": "#1F7A3A",
"label": "12-week streak",
"data": {
"html": "<div class=\"wrap\">\n <p class=\"kicker\">Running · 12 weeks</p>\n <svg viewBox=\"0 0 84 8\" preserveAspectRatio=\"none\" role=\"img\" aria-label=\"Twelve weeks, ten completed\">\n <g fill=\"currentColor\">\n <rect x=\"0\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/><rect x=\"7\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/>\n <rect x=\"14\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/><rect x=\"21\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\" opacity=\".2\"/>\n <rect x=\"28\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/><rect x=\"35\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/>\n <rect x=\"42\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/><rect x=\"49\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/>\n <rect x=\"56\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\" opacity=\".2\"/><rect x=\"63\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/>\n <rect x=\"70\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/><rect x=\"77\" y=\"0\" width=\"6\" height=\"8\" rx=\"1\"/>\n </g>\n </svg>\n <p class=\"note\">10 of 12 weeks · longest run 7</p>\n</div>",
"css": ".wrap { font-family: -apple-system, system-ui, sans-serif; color: #15130F; padding: 1.25rem; }\n.kicker { margin: 0 0 .75rem; font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: #9A8F7E; }\nsvg { width: 100%; height: 2.5rem; color: #1F7A3A; display: block; }\n.note { margin: .75rem 0 0; font-size: .85rem; color: #5C544A; }\n@media (prefers-color-scheme: dark) {\n .wrap { color: #F4EEE2; }\n .note { color: #9A8F7E; }\n}",
"responsive": true
}
}