Clipboard
Shared ComponentsReusable copy action with success, failure, and pending states.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { Clipboard } from "@/components/ui/clipboard";
import { useState } from "react";
import { Input } from "@dashboardpack/core/components/ui/input";
import { Label } from "@dashboardpack/core/components/ui/label";
export default function ClipboardExample() {
const [value, setValue] = useState("https://apex.example/invite/design-team");
return (
<div className="w-full max-w-lg space-y-7">
<div className="space-y-3">
<Label htmlFor="share-link">Workspace invitation</Label>
<Input
id="share-link"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<Clipboard value={value} label="Copy invitation" />
</div>
<div className="rounded-xl border bg-muted/30 p-5">
<p className="mb-4 font-mono text-sm">npm run dev</p>
<Clipboard value="npm run dev" label="Copy command" variant="default" />
</div>
<Clipboard value="unavailable" label="Copy unavailable value" disabled />
</div>
);
}Usage
Pass the exact text to copy and a clear action label. The native Clipboard API requires a secure context and browser permission. A failed copy shows actionable feedback rather than claiming success. Feedback clears after 2.5 seconds and does not apply to a different value. The pending state prevents duplicate clicks. This component copies text; it never cuts or deletes source content.
import { Clipboard } from "@/components/ui/clipboard";Shipped as editable source in src/components/ui/clipboard.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/clipboard.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { Check, Copy } from "lucide-react";
import { Button } from "@dashboardpack/core/components/ui/button";
export interface ClipboardProps {
value: string;
label?: string;
variant?: "default" | "outline" | "ghost";
disabled?: boolean;
onCopied?: (value: string) => void;
className?: string;
}
export function Clipboard({
value,
label = "Copy value",
variant = "outline",
disabled,
onCopied,
className,
}: ClipboardProps) {
const [feedback, setFeedback] = useState<{
value: string;
state: "copied" | "error";
} | null>(null);
const [pending, setPending] = useState(false);
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
useEffect(() => {
if (!feedback) return;
const timer = setTimeout(() => setFeedback(null), 2500);
return () => clearTimeout(timer);
}, [feedback]);
const state = feedback?.value === value ? feedback.state : null;
const copy = async () => {
setPending(true);
try {
await navigator.clipboard.writeText(value);
if (mounted.current) {
setFeedback({ value, state: "copied" });
onCopied?.(value);
}
} catch {
if (mounted.current) setFeedback({ value, state: "error" });
} finally {
if (mounted.current) setPending(false);
}
};
return (
<span className={className}>
<Button
type="button"
variant={variant}
disabled={disabled || pending}
onClick={copy}
>
{state === "copied" ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
{label}
</Button>
<span role="status" className="ms-2 text-xs text-muted-foreground">
{state === "copied"
? "Copied"
: state === "error"
? "Copy failed. Try again or select the text manually."
: ""}
</span>
</span>
);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| value | stringDefault: required | Exact string passed to navigator.clipboard.writeText. |
| label | stringDefault: Copy value | Visible action label. |
| variant | default | outline | ghostDefault: outline | Button visual appearance. |
| disabled | booleanDefault: false | Prevent copying while unavailable. |
| onCopied | (value: string) => voidDefault: — | Called only after the clipboard write succeeds. |
| className | stringDefault: — | Extend the component container with theme-aware utility classes. |