> **building-html-canvases** — skill 16 of 200 in [PostHog/skills](https://skillsdocs.com/PostHog/skills).
>
> Book (all skills, one file): https://skillsdocs.com/PostHog/skills.md
> Machine manifest: https://skillsdocs.com/PostHog/skills/.well-known/agent-skills/index.json
> Install the book: `npx skills add PostHog/skills`
> Upstream: https://github.com/PostHog/skills/blob/main/skills/omnibus/building-html-canvases/SKILL.md @ `main`
> Raw bytes, no header: https://raw.githubusercontent.com/PostHog/skills/main/skills/omnibus/building-html-canvases/SKILL.md
> Base for relative paths: https://raw.githubusercontent.com/PostHog/skills/main/skills/omnibus/building-html-canvases/
> Licence: MIT — https://spdx.org/licenses/MIT.html
>
> Content © its authors, served unmodified. Takedown: https://github.com/DreambaseAI/skillsdocs/issues/new?labels=takedown&title=Takedown+request

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: building-html-canvases
description: >
  Author a PostHog canvas with semantic HTML, CSS, and direct browser APIs — documents, articles,
  generative graphics, 2D canvas and WebGL experiences, and focused experiments where React
  components add no useful structure. Use after building-canvases has routed a canvas request to a
  plain-HTML/browser-API implementation. Covers the thin component wrapper the current runtime
  requires, styling and theming without Quill, drawing surfaces, and animation/cleanup patterns.
---

# Building HTML canvases

Some canvases are documents or graphics programs, not applications: a written report, a diagram,
a generative-art piece, a WebGL scene. For these, semantic HTML, CSS, and direct browser APIs are
the right tools — don't force Quill components or React state onto a static page.

## The wrapper the current runtime requires

Every canvas keeps `src/canvas.tsx` as its mounted React entry component (default export, no
props). Keep the React layer as a thin shell and write the
experience in HTML/CSS/browser APIs inside it:

- A document is JSX that is effectively semantic HTML — `<article>`, headings, lists, tables,
  figures — with a `<style>` block for typography and layout. Write real, specific copy.
- A drawing/WebGL program renders a `<canvas>` element and drives it imperatively from a
  `useEffect` via a ref: get the 2D/WebGL context, run the setup and render loop there.
- Clean up in the effect's return: cancel `requestAnimationFrame` loops, remove listeners, and
  release contexts, so theme switches and remounts don't leak or double-run.
- Mixing tiers is fine: a mostly static page can mount one interactive island, and a data board
  can hand a chart's `<canvas>` to imperative code while React owns the chrome.

The import allowlist still applies (react, react-dom, @posthog/quill, recharts, lucide-react,
dayjs) — browser globals (`document`, `CanvasRenderingContext2D`, `WebGLRenderingContext`,
`requestAnimationFrame`, `IntersectionObserver`, Web Audio, etc.) need no import. Three.js and
other npm graphics libraries are not yet loadable; write against raw WebGL or 2D canvas until the
build pipeline's dependency admission ships.

## Styling and theme without Quill

- Size the outermost JSX/HTML element to the iframe viewport with `min-h-screen` or `min-height: 100vh`, so it fills the viewport and grows past it as content demands.
  Do not use `h-screen` or `height: 100vh` there: a fixed viewport height caps a flex column, so tall children shrink and clip instead of scrolling.
  Do not use `h-full` or `height: 100%` on that root either: a published canvas's artifact shell gives its `html`, `body`, and `#root` elements no explicit height, so percentage height collapses to the content height.
  A `min-height` root is not a definite height, so a descendant's percentage height still collapses; give an intermediate wrapper an explicit height when a child must fill a box.
- Use Tailwind utilities and/or a `<style>` block (keyframes and complex selectors are fine).
- The host toggles a `.dark` class on the document root when the user's PostHog theme changes.
  Define your colors as CSS variables under `:root { … }` with overrides under `html.dark { … }`,
  or use theme token utilities (`bg-background`, `text-foreground`, `border-border`) — never a
  light-only hardcoded color.
- Give your own CSS variables a prefix (`--doc-bg`, `--doc-muted`). Never reuse a platform token
  name: the bundled Quill stylesheet sets `--background`, `--border`, `--card`, `--chrome`,
  `--input`, `--muted`, `--primary`, and `--fill-*` on every element, so a `:root` or `html.dark`
  value with one of those names never reaches any element. A page that colors its text with its own
  `--muted` then renders unreadable (pale text on a pale page). Validation rejects such a
  declaration with `platform_token_redeclared`.
- For canvas/WebGL drawing colors, read the resolved token at runtime
  (`getComputedStyle(document.documentElement).getPropertyValue("--primary")`) or your own CSS
  variables, and re-read on theme change if the scene is long-lived.

## Rules that still apply

- PostHog data comes only through the `ph` bridge (see `querying-canvas-data`), including
  `ph.capture` for interaction analytics. Other requests and external styles, images, fonts, media,
  or frames require their exact public HTTPS origins in `capabilities.network.origins` and work only
  after publishing. Remote scripts and dynamic imports remain blocked.
- A document that states PostHog numbers must make each one verifiable: an insight-backed number
  links its saved insight through `ph.openExternal` (URL from the `generate-app-url` MCP tool,
  from a click); an ad-hoc `ph.query` number discloses the exact query that ran in a `<details>`
  element beside the claim — see "Verifiability" in `querying-canvas-data`.
- External links go through `ph.openExternal(url)` (posthog.com origins only), from a user
  interaction.
- Validate and publish through the canvas tools as described in `validating-and-publishing-canvases`.
