posthog/building-react-quill-canvases
> 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.
npx skills add https://github.com/PostHog/posthog --skill 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.
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.
A PostHog data board must be built entirely from @posthog/quill components — never a native
control or a styled <div> standing in for one:
Select (never a native <select>); button → Button (never <button>);text field → Input/Textarea; checkbox → Checkbox; label → Label.
Table (TableHeader > TableRow > TableHead, then TableBody > TableRow > TableCell);panel → Card (CardHeader + CardTitle + CardContent); pill → Badge; titles → Heading; body → Text.
<div>s and recharts elements.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.
style;use their variant/size props. Put layout utilities (flex, grid, gap-4, p-4) on your own
wrapper <div>s.
variant="outline"; variant="primary" for the one main action only.style for genuinely dynamicruntime values (fixed sizes use arbitrary-value utilities like h-[280px]).
.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.
bg-success) is a pale background filland -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.stroke="var(--primary)", grid/axes invar(--border)/var(--muted-foreground)).
\uXXXX escapes render verbatim in JSX text.
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, and handle the empty/error case.
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.
Take posthog/building-react-quill-canvases from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.