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.

FieldTypeNotes
variant"v1"optionalDesign variant to render (defaults to v1).
htmlstringrequired1–200000 chars
cssstringoptionalup to 100000 chars
responsivebooleanoptionaldefault 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 css field, not inline style= 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. html panes get the device's available space, not a scroll view.
  • No JavaScript, ever: <script> and on* handlers do not execute. Everything must be static. If you need interaction, model it as a structured pane type instead.
  • Set responsive: false only when the content has a fixed natural size (a small diagram); the default true is almost always right.

Avoid

  • Don't rebuild a card a structured type already gives you — an html KPI is strictly worse than a kpi.
  • 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 px widths, absolute positioning, or viewport units that assume a desktop canvas.
  • Don't put walls of text in an html pane 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
  }
}