Actions

Copy Button

Copy exactly what you need, with success and failure feedback.

OSCL StudioComponents, in context.Live preview
FOUNDATION / ACTIONS

Copy Button

Hello from OSCL
Interactive preview. Real components.View docs

Use it in your project

Install oscl-ui from npm and import oscl-ui/styles.css once. See installation.

copy-button-example.tsx
import { CopyButton } from "oscl-ui";

<CopyButton value="Hello from OSCL" label="Copy message" />

Accessibility

Keep visible labels and keyboard focus. Supply an accessible name to icon-only actions. Try this preview with Tab, Enter, Space, and Escape where appropriate.

The source, in full

This is the actual source from the isolated OSCL package used by this website. Native props and TypeScript declarations are included in the npm package.

copy-button.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import type { ButtonHTMLAttributes } from "react";
import { Button } from "./button.js";

export interface CopyButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
  value: string;
  label?: string;
}
export function CopyButton({ value, label = "Copy", className = "", onClick, ...props }: CopyButtonProps) {
  const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
  const [pending, setPending] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const mounted = useRef(true);
  useEffect(() => { mounted.current = true; return () => { mounted.current = false; clearTimeout(timer.current); }; }, []);
  async function copy() {
    clearTimeout(timer.current);
    setPending(true);
    try {
      if (!globalThis.navigator?.clipboard?.writeText) throw new Error("Clipboard unavailable");
      await navigator.clipboard.writeText(value);
      if (mounted.current) setStatus("success");
    } catch {
      if (mounted.current) setStatus("error");
    } finally {
      if (mounted.current) {
        setPending(false);
        timer.current = setTimeout(() => setStatus("idle"), 3000);
      }
    }
  }
  const feedback = status === "success" ? "Copied to clipboard." : status === "error" ? "Copy failed. Select and copy the text manually." : "";
  return <span className={`oscl-copy ${className}`}>
    <Button {...props} variant="ghost" size="sm" disabled={props.disabled || pending} onClick={event => { onClick?.(event); if (!event.defaultPrevented) void copy(); }}
      aria-label={props["aria-label"] || label} leadingIcon={<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2" /><path d="M16 8V4H4v12h4" /></svg>}>
      {status === "success" ? "Copied" : status === "error" ? "Retry" : label}
    </Button>
    <span className={status === "error" ? "oscl-copy-error" : "oscl-sr-only"} role="status" aria-live="polite">{feedback}</span>
  </span>;
}