Copy & download
StableCopy text with a toast and a fallback when the clipboard is blocked, and save text as a file.
useCopy copies inside the click (browsers require a user gesture) and confirms with a toast. When the clipboard is blocked, as in an iframe without permission, it hands you the request and CopyFallback shows the text already selected, so the keyboard shortcut still works. saveTextFile saves text as a file: through the claude.ai downloads capability inside an artifact, otherwise with a temporary link.
Updated View source
Copy or download
import { useRef, useState } from 'react'import { CopyFallback, saveTextFile, useCopy } from '@rchursin/product/iconset/copy'import { Button } from '@rchursin/product/ui'
/** * useCopy copies inside the click and confirms with a toast. When the clipboard is blocked (an * iframe without permission, an old browser), `blocked` is set: show CopyFallback, which selects * the text so Ctrl C or ⌘ C still works. saveTextFile saves the same text as a file, with its * media type. */const settings = { theme: 'system', density: 'compact', weekStartsOn: 'monday', shortcuts: true }const json = JSON.stringify(settings, null, 2)
export default function CopyAndSave() { const { copy, blocked, dismiss } = useCopy() const [saved, setSaved] = useState<string | null>(null) const copyButton = useRef<HTMLButtonElement>(null)
function closeFallback() { dismiss()Usage
import { useCopy, CopyFallback, saveTextFile } from '@rchursin/product/iconset/copy'const { copy, blocked, dismiss } = useCopy()<Button onClick={() => copy(svg, 'SVG')}>Copy SVG</Button>{blocked && <CopyFallback request={blocked} onDone={dismiss} />}<Button onClick={() => saveTextFile('calendar.svg', svg, 'image/svg+xml')}>Download</Button>Examples
The blocked-clipboard fallback
Pretends the clipboard refused, so you can see the fallback in any browser.
import { useRef, useState } from 'react'import { CopyFallback, type CopyRequest } from '@rchursin/product/iconset/copy'import { Button } from '@rchursin/product/ui'
/** * What a visitor sees when the clipboard refuses. Here the refusal is pretended, so you can see * the fallback in any browser: the text appears in a read-only field, focused and selected. * Done calls onDone; this example then hands focus back to the button that opened it. */const request: CopyRequest = { text: 'npm create vite@latest agent-dashboard -- --template react-ts', what: 'command' }
export default function CopyFallbackDemo() { const [open, setOpen] = useState(false) const opener = useRef<HTMLButtonElement>(null)
function done() { setOpen(false) opener.current?.focus() }
return ( <div className="flex w-full max-w-md flex-col gap-3"> <Button ref={opener} variant="secondary" icon="clipboard" className="self-center" onClick={() => setOpen(true)}> Show the fallback </Button> {open && <CopyFallback request={request} onDone={done} />} </div> )}API
useCopy()
| Prop | Type | Default | Description |
|---|---|---|---|
returns | { copy: (text: string, what: string) => Promise<void>; blocked: CopyRequest | null; dismiss: () => void } | – | copy() writes to the clipboard and toasts “<what> copied” (needs ToastProvider; without it, nothing shows). On failure it sets blocked to { text, what } instead of throwing. |
CopyFallback props
| Prop | Type | Default | Description |
|---|---|---|---|
requestrequired | CopyRequest | – | { text, what }, usually blocked from useCopy. |
onDonerequired | () => void | – | Called by its Done button. Move focus back to the control that copied. |
className | string | – | Extra classes on the panel. |
saveTextFile(filename, text, type?) · downloadText(filename, text, type?)
| Prop | Type | Default | Description |
|---|---|---|---|
filenamerequired | string | – | The name to save as, extension included. |
textrequired | string | – | The file’s contents. |
type | string | 'text/plain' | The file’s media type for the temporary link: "image/svg+xml" for an icon (as IconDetail passes), "application/json" for settings. The artifact viewer gets only the name and the text. |
returns | Promise<'saved' | 'declined' | 'unavailable'> | – | declined when the person cancelled in the artifact viewer, unavailable when the host can’t save. Outside an artifact it always resolves saved. downloadText is the temporary link on its own and returns nothing. |
Accessibility
- The toast confirms a copy in words, and toasts are announced politely.
- CopyFallback focuses its field and selects the text on open, and its label says which keys copy it (⌘C or Ctrl+C).
- When the fallback closes, the focused element disappears with it: send focus back to the button that copied, as both examples and the icon set’s copy buttons do.
Guidelines
Do
- Name what was copied: “SVG copied”, not just “Copied”.
- Render CopyFallback whenever blocked is set; clipboard access fails more often than you expect.
Don’t
- Don’t copy on page load or on hover; browsers require a click.
- Don’t show “Copied” before the promise resolves.
In use
Related
- Copy buttonCopy text to the clipboard with one press: a pearl button that says “Copied”, a quiet one for code, and the helper under both.
- Icon browserSearch all 161 icons by name or use, check them at 16 to 24 pixels, and copy one as SVG, JSX or its name.
- ToastShort past-tense results with at most one action, like “Run archived · Undo”. Read out politely; they wait while hovered or focused.
Source
The component’s own files, exactly as they run on this site.
/** * Copying and saving text. The clipboard call happens inside the click (browsers require a * user gesture). Where the clipboard is blocked (an iframe without permission, an old browser), * the text appears in a read-only field, already selected, so the keyboard shortcut still works. */import { useCallback, useEffect, useId, useRef, useState } from "react"import { cx } from "../../components/cx"import { modKey } from "../lib/hotkeys"import { Button } from "../ui/button"import { useToast } from "../ui/toast"
export interface CopyRequest { text: string /** What was copied, for the toast and the fallback ("SVG" → "SVG copied") */ what: string}
export function useCopy() { const toast = useToast() const [blocked, setBlocked] = useState<CopyRequest | null>(null)