Omnibus
200 skills · 1230 min
Omnibus
Skill 18 of 200
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…
6 minutes · 1,402 words · 11 sections
Install
npx skills add PostHog/skills --skill building-react-quill-canvasesnpx skills add PostHog/skills/plugin marketplace add PostHog/skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
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 (opens in a new tab) on a first build: it already wires the date picker, theme tokens, per-query loading state (every card fills in independently as its own data lands), and correct typed-node result reading. Keep that wiring; replace the sample metrics and layout.
Use React, Quill, Recharts, Lucide, and Day.js for the standard application shell. The platform also admits ten optional libraries for specialized work. Read references/platform-libraries.md (opens in a new tab) before choosing one.
PostHog data comes through import { ph } from "@posthog/canvas-sdk" — a platform-provided
module, so it needs no dependencies entry. The same object exists as the window.ph global
(how existing canvases reach it); prefer the import in new code.
Other bare imports, dynamic import(), require(), <script> tags, and remote code fail
validation. Direct network requests and external images, fonts, media, or frames require an exact
HTTPS origin in capabilities.network.origins. They work only in the published canvas — the
edit-mode preview blocks direct network access regardless of declaration. Stylesheets from declared
origins are allowed; remote scripts remain blocked, so bundle code with the canvas.
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.Table inside a Card needs <Card size="sm" flush> around <Table size="sm" fullWidth>.
Without flush the card’s padding insets the table, so its header and rows stop meeting the card’s edges.
flush drops the card’s section gap, bottom padding, and the CardContent inline padding, while the header keeps its own padding and divider.
Matching size="sm" on both lines the table’s edge columns up with the card’s title.<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.min-h-screen (min-height: 100vh) so it fills the iframe viewport and grows past it as content demands.
Do not use h-screen there: a fixed viewport height caps a flex column, so tall children shrink and clip instead of scrolling.
Do not use h-full there either: a published canvas’s artifact shell gives its html, body, and #root elements no explicit height, so a percentage root height collapses to content height.overflow-y-auto: once it grows past the viewport the iframe’s own document scrolls.min-height root is not a definite height, so h-full on a direct child still collapses to content height.
Give an intermediate wrapper an explicit height (h-[280px]) when a child must fill a box.style for genuinely dynamic
runtime 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 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.--background, --border, --card,
--chrome, --input, --muted, --primary, --fill-*), in a stylesheet or a <style> block.
Quill sets those on every element, so your value never applies and text can turn unreadable.
Prefix your own variables (); validation rejects the collision with
.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.
Render progressively — each query owns its loading state. The chrome (heading, date picker,
card frames with skeletons inside) renders immediately, every independent query fires
concurrently on mount, and each card swaps its skeleton for data the moment its own query
resolves, so a slow query only holds back its own card. Never drive the whole canvas off one
shared loading flag or a Promise.all across independent queries — that makes the fastest
metric wait for the slowest. Set each section’s loading state true again on refresh; never show
a blank or a jumping layout. Content the first paint doesn’t show (an inactive tab, a collapsed
section, a drill-down) defers its query until the user reveals it.
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.
Treat these as starting shapes and adapt them to the request and available data.
Heading, then a responsive grid of compact Card KPIs, trend charts, and useful
breakdown tables.Badge delta for KPIs when a meaningful comparison period exists.LineChart for time series and a BarChart for discrete categories. Do not turn every
result into a table.ph.openExternal with a URL minted by generate-app-url); an ad-hoc
ph.query card gets a “View query” Dialog or Collapsible showing the exact query that ran —
see “Verifiability” in querying-canvas-data.Use this shape when the request or legacy requested pattern says web-analytics:
Use the web analytics query kinds described in querying-canvas-data; do not recreate bounce rate,
sessionization, attribution, or unique visitors in HogQL. Format large values for display, such as
236K, while preserving the raw value for calculations and accessible labels.
Use controlled Quill inputs for each dimension, event, or date choice. Keep result sets small and refresh every dependent query when a control changes.
For a checklist, QA runbook, launch plan, onboarding sequence, or any list of steps people work
through and tick off, start from the complete, validated project in
references/checklist-example.md (opens in a new tab). Its load-bearing parts — the
typed content module separate from the component, one shared ph.state key per step, the
debounced ref-alongside-state update path, an expected outcome on every step, and visible
load/save failure states — are what break when improvised. Keep them; replace the content.
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.
Persisting values across reloads (ph.state, with user/shared scopes) and writing into PostHog
from a button (ph.actions.invoke, e.g. filing a task or an annotation) have their own API
contracts, capability declarations, and gesture rules — read the “Runtime memory” and “PostHog
writes” sections of the querying-canvas-data skill before using either. Wire actions to a
Button onClick that disables itself while the call is in flight, and render the returned
error (they are real PostHog writes, throttled server-side).
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.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
main, last pushed 24 September 2026.SKILL.md, not by matching a directory convention. 2 distinct layouts observed: skills/omnibus/*/SKILL.md, skills/posthog/all/skills/*/SKILL.md.h1 and no skipped levels:--doc-mutedplatform_token_redeclaredstroke="var(--primary)", grid/axes in
var(--border)/var(--muted-foreground)).\uXXXX escapes render verbatim in JSX text..claude-plugin/marketplace.json by PostHog, declaring 6 plugins. It is read for editorial metadata only — never as the skill index, which is always the repository tree./PostHog/skills.md, and each skill at its own .md URL.3 files · 31 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 18.
Documentation the agent loads on demand, rather than up front.