Deploy Cursor canvases to the web.
A Cursor canvas is a .tsx that imports only from cursor/canvas. That module isn't an installable package; it's provided by the Cursor IDE at compile time, so a canvas can't build or render anywhere outside the IDE. This project is an open, Mantine-backed shim of cursor/canvas. A build-time Vite alias swaps the module for the shim, so the same .tsx renders in the Cursor IDE and on the web. No converter, no fork of your source.
Live example: https://thisismydesign.github.io/cursor-canvas-web/
How it works: a module shim, not a converter
Your canvas stays a normal .tsx React component that imports only from
cursor/canvas. The only bridge is one module that re-exports that API
implemented with Mantine. A Vite alias (plus a matching tsconfig path) resolves
cursor/canvas to the shim at build time. There is no parser, AST, or registry,
and your canvas source never changes.
your.canvas.tsx Vite alias + @thisismydesign/cursor-canvas-web
imports "cursor/canvas" -> tsconfig paths -> Mantine core + @mantine/charts
-> Vite build -> any static host
Because the shim mirrors the real SDK API, a canvas that builds here also compiles and renders unchanged in the Cursor IDE.
Install
pnpm add @thisismydesign/cursor-canvas-web
# peer deps your app must supply:
pnpm add react react-dom @mantine/core @mantine/charts rechartsThe package ships two entry points:
| Import | Provides |
|---|---|
@thisismydesign/cursor-canvas-web |
The cursor/canvas shim (components, hooks). |
@thisismydesign/cursor-canvas-web/runtime |
CanvasRoot + mountCanvas web host glue. |
Use it in your project
1. Point cursor/canvas at the package
Two coordinated settings — runtime (Vite) and types (tsconfig) — keep your canvas sources pure while resolving the bare specifier to the shim:
// vite.config.ts — runtime resolve: { alias: { 'cursor/canvas': '@thisismydesign/cursor-canvas-web', }, }
2. Mount your canvas
Use the mountCanvas helper (it wraps MantineProvider for you):
// src/main.tsx import { mountCanvas } from '@thisismydesign/cursor-canvas-web/runtime'; import '@mantine/core/styles.css'; import '@mantine/charts/styles.css'; import MyCanvas from '../.cursor/canvases/my.canvas'; mountCanvas('root', <MyCanvas />);
Or, if you manage your own React root, wrap the tree with CanvasRoot:
import { CanvasRoot } from '@thisismydesign/cursor-canvas-web/runtime'; import '@mantine/core/styles.css'; import '@mantine/charts/styles.css'; createRoot(el).render( <CanvasRoot defaultColorScheme="auto"> <MyCanvas /> </CanvasRoot>, );
The canvas file itself never changes — it still imports only from
cursor/canvas, so it renders identically in the Cursor IDE.
3. Deploy
The result is a plain Vite app, so pnpm build produces a static dist/ you
can host anywhere (GitHub Pages, Netlify, Vercel, S3, …). This repo deploys its
example canvas to GitHub Pages — see Example deployment.
Supported canvas API
The shim implements the full public cursor/canvas surface:
- Typography:
H1,H2,H3,Text,Code,Link - Layout:
Stack,Row,Spacer,Grid,Divider,mergeStyle - Surfaces:
Card/CardHeader/CardBody(incl.collapsible) - Display & feedback:
Stat,Pill,Callout,Table - Actions:
Button,IconButton - Forms:
Select,Checkbox,Toggle,TextInput,TextArea - Charts:
LineChart,BarChart,PieChart - Rich:
Swatch,UsageBar,CollapsibleSection,TodoList/TodoListCard,DiffView/DiffStats,computeDAGLayout - Hooks:
useCanvasState,useHostTheme,useCanvasAction - Tokens:
colorPalette,usageColorSequence,canvasTokens(Light),canvasPalette(Dark|Light)
Known fidelity gaps (behavior is reproduced, not the exact renderer):
DiffView skips Shiki syntax highlighting (plain-text fallback), and
useCanvasAction is a no-op on the web (IDE-only). Styling approximates the
Cursor theme rather than matching it pixel-for-pixel.
If your canvas needs an export the shim doesn't yet provide, see Extending the shim.
Example deployment
This repo is itself a working example. The canvas at
.cursor/canvases/demo.canvas.tsx is the
single source of truth: the web app imports it directly, and the same file is
copied into Cursor's managed canvases folder so it renders in the IDE too.
The deployed result lives at https://thisismydesign.github.io/cursor-canvas-web/.
deploy.yml builds and publishes to GitHub Pages on push to main via
configure-pages / upload-pages-artifact / deploy-pages. The Vite base
defaults to /cursor-canvas-web/ (project Page); override it with the
BASE_PATH env var if you deploy elsewhere.
Develop this repo
Tool versions (Node, pnpm) are pinned in .tool-versions
and managed with mise.
mise install # install Node + pnpm from .tool-versions pnpm install pnpm dev # start the dev server
Validation gate:
pnpm typecheck # tsc --noEmit, strict pnpm lint # eslint pnpm test # vitest run pnpm build # typecheck + vite build -> dist/
Layout
| Path | Role |
|---|---|
.cursor/canvases/demo.canvas.tsx |
The example canvas; imports only cursor/canvas. |
src/shim/cursor-canvas.tsx |
Mantine implementation matching the real SDK API. |
src/shim/theme.ts |
Tone→color map + host-theme tokens. |
src/runtime/index.tsx |
Web host glue (CanvasRoot, mountCanvas). |
src/main.tsx |
Mounts React inside MantineProvider. |
vite.config.ts |
resolve.alias for cursor/canvas + Pages base. |
vite.lib.config.ts |
Library build (shim + runtime) for publishing. |
tsconfig.json |
paths maps cursor/canvas to the shim. |
Extending the shim
To support more of the canvas API:
- Add the export to
src/shim/cursor-canvas.tsx, implemented with Mantine. - Keep prop shapes stable — they are the contract canvases rely on, and they must match the official SDK declarations so canvases render unchanged in the IDE.
- If it carries semantic color, map
toneviatoneColorinsrc/shim/theme.ts, using the SDK's tone vocabulary for that primitive (StatTone,PillTone,CalloutTone,ChartTone). - Add a test under
src/shim/__tests__/.
The only non-trivial adapter so far is LineChart, which reshapes parallel
categories + series[] arrays into Mantine's array-of-row-objects data and
maps each series tone to a color (reshapeLineChartData).
Publishing
prepublishOnly runs typecheck, test, and build:lib automatically, so a
publish only needs:
npm login npm publish --access public
--access public is required for the scoped package. Publishing needs 2FA
(WebAuthn/passkey) on your npm account; npm publish opens a browser to
complete the passkey prompt.
