PostHog logo

Skill

building-react-quill-canvases

build React and Quill canvases

Covers React UI Components Frontend Design

Description

Author the React + Quill implementation of a PostHog canvas: the single-component contract, the allowed imports, Quill (PostHog's design system) component and composition rules, theme-aware design tokens, loading skeletons, and the in-canvas date picker. Use after building-canvases has routed a canvas request to a React implementation — dashboards, data boards, forms, tools, or any canvas that should look native to PostHog.

SKILL.md

Building React + Quill canvases

The whole application is one React/TSX file (src/canvas.tsx in the source project). It must export default a single React component that takes no props — the host mounts it. Do not import react-dom or call createRoot.

Start from the working scaffold in references/starter-scaffold.md on a first build: it already wires the date picker, theme tokens, per-card skeletons, and correct typed-node result reading. Keep that wiring; replace the sample metric and layout.

Imports

Import only from: react, react-dom, react-dom/client, @posthog/quill, recharts, lucide-react, dayjs. Anything else — including dynamic import(), require(), fetch(), <script> tags, or remote code — fails validation. Use @posthog/quill for UI, recharts for charts, lucide-react for icons, dayjs for dates.

Quill component rules

A PostHog data board must be built entirely from @posthog/quill components — never a native control or a styled <div> standing in for one:

  • Dropdown/picker → Select (never a native <select>); button → Button (never <button>); text field → Input/Textarea; checkbox → Checkbox; label → Label.
  • Table → Table (TableHeader > TableRow > TableHead, then TableBody > TableRow > TableCell); panel → Card (CardHeader + CardTitle + CardContent); pill → Badge; titles → Heading; body → Text.
  • The only non-Quill tags allowed are plain layout <div>s and recharts elements.
  • Quill is built on Base UI: compose compound parts (Select + SelectTrigger/SelectContent/SelectItem), use controlled value + onValueChange, and swap a part's element with the render prop (e.g. <PopoverTrigger render={<Button …/>} />) instead of wrapping it.
  • Quill components are already themed — never restyle one with Tailwind classes or inline style; use their variant/size props. Put layout utilities (flex, grid, gap-4, p-4) on your own wrapper <div>s.
  • Buttons: default to variant="outline"; variant="primary" for the one main action only.

Styling and theme

  • Style with Tailwind utilities and Quill components; reserve inline style for genuinely dynamic runtime values (fixed sizes use arbitrary-value utilities like h-[280px]).
  • The canvas follows the user's PostHog theme; a .dark class on the document root flips at runtime. Color only from the design-token utilities — surfaces bg-background bg-card bg-muted bg-primary bg-success bg-warning bg-info bg-destructive; text text-foreground text-muted-foreground text-card-foreground; borders border-border. Never a hardcoded hex or light-only color.
  • Status tokens invert the usual convention: the bare token (bg-success) is a pale background fill and -foreground (text-success-foreground) is the strong readable color. Colored text or icons always use the -foreground utility; a filled pill pairs bg-success text-success-foreground. Prefer the Quill Badge (variant="success"/"destructive") for deltas so you don't hand-pick.
  • bg-secondary, text-secondary, bg-accent, and bg-popover are not defined in the canvas — avoid them.
  • recharts strokes/fills use token CSS variables (stroke="var(--primary)", grid/axes in var(--border)/var(--muted-foreground)).
  • Write Unicode glyphs (curly quotes, ellipsis, arrows, emoji) as literal characters in JSX — \uXXXX escapes render verbatim in JSX text.

Loading, error, and empty states

Every data point renders a skeleton in its own Card while loading or refreshing: SkeletonText (matching lines and text-size className) for text/number values, Skeleton for blocks/charts. Drive isLoading off the data calls and set it true again on refresh; never show a blank or a jumping layout.

A failed query and an empty result are different states — never let one render as the other. .catch on every ph.query/ph.loadInsight must set an error state that renders visibly (the message plus a Retry button wired to the refresh nonce, as in the starter scaffold), not fall through to zeros, an empty chart, or a "no data yet" message. A query that silently swallows its error makes real breakage (a missing table, an auth failure, a bad query) look like missing data. Reserve the empty state for a query that succeeded with no rows.

Date window

A data board owns its own date control — render Quill's DateTimePicker (never a custom Select or native date input) inside a Popover whose trigger is a Quill Button. PopoverContent gets exactly className="w-auto p-0" and nothing is added to DateTimePicker beyond value/onApply/onCancel (it self-sizes; don't pass compact or widths). Re-run every query when the window changes — see the querying-canvas-data skill for feeding it into dateRange.

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.