artifact-design
Page contract — read before your first publish
These are the Artifact tool's own rules for the file you publish; the design guidance below builds on them.
Format: Always author the page as .html. Publish a .md file only when a loaded skill explicitly instructs it. When the user shares a markdown document or asks to turn one into an artifact, author an HTML page based on its content — preserve its substance, and design the page as you would any other artifact rather than transcribing the markdown one-to-one.
Skeleton: The file is wrapped in a <!doctype html>…<head>…</head><body> skeleton at publish time, so write the page content directly — no <!DOCTYPE>, <html>, <head>, or <body> tags of your own. Its head carries only a charset and viewport meta (with viewport-fit=cover) plus a small reset — light color-scheme, :root padded top and bottom by the phone's safe-area insets, zero body margin with a 14px system font, img{max-width:100%}, and [hidden]{display:none!important} (toggle visibility with el.hidden, not style.display) — so put your own <title> and <style> at the top of the file. Keep the :root padding. Put <meta name="claude:viewport-fit-sides" content="cover"> at the top of the file beside your <title>: it promises that the side gutter uses the two max() paddings given under Responsive, so the tag goes in only together with both paddings, never alone. An edit of an older page that has neither leaves both out, unless the ask redoes the page's layout. With it the page also runs under a system bar at the phone's side, where backgrounds reach that edge while the gutter keeps text and controls clear. Anything fixed to a screen edge stays at 0 and adds that edge's env(safe-area-inset-*, 0px) to its own padding (one that spans the width takes the side gutter too), a floating or absolutely placed control adds the nearby edges' insets to its offsets, and a sticky page header uses top: env(safe-area-inset-top, 0px), not 0, with the band above it painted by a fixed body::before (content: ""; position: fixed; inset: 0 0 auto; height: env(safe-area-inset-top, 0px); z-index: 10) in its background, or in the page's where it starts lower.
Title: Put a <title> at the top of the HTML; only the first 8KB of the file is scanned for it. It is the artifact's name in the browser tab and the gallery, so write a name, not a summary: a short noun phrase, typically two to four words, specific enough to pick this page out among many, the way an app or a document is named. When the user already has a specific name for the thing, use that name for the title rather than coining a new one. Never use a generic category label alone, and never append an explainer after a dash or colon. If you shorten a title that pairs the name with a generic word, keep the name, not the generic word. A multi-word title that already reads as one specific name is finished; do not shorten it further. The explanation goes in the one-sentence description parameter, which becomes the gallery card's subtitle. The title parameter fills in only when an HTML file has no <title> tag (Markdown pages keep their filename). Keep the title stable across redeploys.
External resources — CDN allowlist (CSP-enforced): external scripts load ONLY from https://cdnjs.cloudflare.com (preferred), https://cdn.jsdelivr.net/npm/, https://unpkg.com, https://cdn.tailwindcss.com (Tailwind's play-CDN script) and https://code.jquery.com; external stylesheets ONLY from https://fonts.googleapis.com, with the font files they pull from https://fonts.gstatic.com (give every face a real fallback stack). Everything else is blocked, with no visible error: every other host (esm.sh included) and, even on those CDNs, anything but a script — stylesheets, images, media, fetch/XHR/WebSocket, a library's runtime fetches. So inline all other CSS and JS and embed assets as data: URIs. How to load a library: <script src="https://cdnjs.cloudflare.com/ajax/libs/<lib>/<exact version>/<file>"> — pick the UMD build, which defines a global (e.g. react/18.3.1/umd/react.production.min.js, then react-dom) — placed BEFORE any inline <script> that uses it. Pin every library URL to an exact version (react@18.3.1 on jsDelivr or unpkg, react/18.3.1/… on cdnjs, cdn.tailwindcss.com/3.4.17), never a bare name, a range or a tag (react, react@18, @latest). Choose a version at least two weeks old. Any version you knew before this conversation is old enough; if you look versions up, skip any published in the last two weeks. The viewer's sandbox also blocks any download the page starts itself — <a download> links (data:/blob: hrefs included) and script-driven saves are inert for viewers — so never offer a file through a plain link. Links to other websites (https://…) open in a new tab, but email, phone and app links (mailto:, tel:, sms:, other custom schemes) are unreliable inside an artifact: for many viewers (for example anyone outside the user's organization, or anyone viewing through a public link) following one, by link or by script, often does not work, and the page cannot tell whether it did. So show the address or number itself as selectable text (a copy button helps), treat such a link as a convenience that may do nothing, and never tell the viewer a message was sent or a call placed because the viewer tapped one. Artifacts render mermaid diagrams natively — markdown via ```mermaid fences, HTML via <pre class="mermaid"> blocks — no library needed, don't load one. The viewer never shows alert(), confirm() or prompt() dialogs — confirm() returns false and prompt() returns null immediately — so build any confirmation step into the page itself.
What the viewer's frame allows: The page runs in a locked-down frame; what it refuses below, it refuses for every viewer (anonymous, signed-in, embedded, desktop and mobile apps), so build around these limits instead of detecting them. The page cannot open the print dialog — window.print() does nothing — so never offer a Print or "Save as PDF" button. Forms work as page UI (inputs, validation, submit events), but a real submission has nowhere to go: handle submit in script with preventDefault() and never point action at another site or a mailto: address. Copy buttons work when navigator.clipboard.writeText is called inside the click handler — catch its rejection (older desktop apps and some app views refuse it) and fall back to selecting the text; reading the clipboard never works, though the viewer's own Paste (the paste event) does. Camera, microphone, screen capture, location, motion, Web Share and similar device APIs are refused without a prompt — build on one only when this user has a runtime capability for that device (the artifact-capabilities skill lists them) and the page declares it; otherwise take photos, audio and data as uploaded files instead (file inputs, drag-and-drop of files and FileReader work in browsers). A screen wake lock may be granted while the page is visible: request it and tolerate rejection. Fullscreen and pointer lock work from a click in desktop browsers; treat both as optional (handle the rejection) since phones and some app views lack them. Sound plays only after the viewer interacts (muted autoplay is fine), so start audio from a button. Other sites cannot be embedded — no YouTube, map or form iframes, and no <object>/<embed>; link out instead: links open outside the artifact, normally in a new tab, while window.open works only for some signed-in viewers in the artifact's own organization and returns null for everyone else, so use real <a href> links. Web Workers work from your own files or blob: URLs; service workers and WebRTC do not. fetch() of files published alongside the page works with relative URLs; images from your own files, data: or blob: URLs draw to canvas and export cleanly. Only a plain #anchor (letters, digits, . _ ~ -) from the artifact's link reaches location.hash — never #/path, #key=value state or the query string — and the page's own URL changes (pushState, path routes, its hash) never show in the viewer's address bar. Only where a page has several views worth bookmarking or sending (tabs, sections, records), name each with a bare token (never settings, sidebar, customize, upgrade, downloads or anything starting comment-, which the viewer keeps), open the one in location.hash on load, keep all other state in the page, and give the user each view's link (the artifact's link plus #token), because the address bar will not follow them from view to view.
Browser storage: localStorage (also sessionStorage and IndexedDB) works, but each artifact has its own origin and the data lives only in that viewer's browser — it survives republishes to the same URL and never reaches other viewers, other devices, or Claude. It can come back empty or the accessor can throw (a private window, cleared or blocked site data, previews or thumbnail capture), so wrap every read and write in try/catch and render the page correctly without it. Use it only for per-viewer conveniences (a remembered tab or filter, a collapsed section, an unsent draft), never for state that must persist reliably, be shared between viewers, or be read back by Claude — state like that belongs in a runtime capability when this user has one: load the artifact-capabilities skill before writing the page.
Size: The rendered page must be 16MB or smaller, and embedded data: URIs count toward that.
Responsive: The page must also work at phone width (about 400px), and the page body must never scroll horizontally. Keep a side gutter of at least 16px at every width, written padding-left: max(16px, env(safe-area-inset-left, 0px)) and padding-right: max(16px, env(safe-area-inset-right, 0px)) so that it also clears a system bar at the phone's side: set it once on body or one outer wrapper (where colored bands span the width, on each band's inner wrapper), and give that element its vertical padding with padding-block, never a padding shorthand that zeroes the sides. Use relative units. Let flex and grid rows wrap or stack to one column when narrow, and give any flex or grid child that holds running text, code or a table min-width: 0, so long content wraps or scrolls inside it instead of pushing the page wider. Put max-width: 100% on images and on any aspect-ratio box, and give nothing a min-width wider than the screen. Only tables, diagrams and code blocks may be wider, each inside its own overflow-x: auto container.