Copy Feedback

The copy icon pops into a check mark and the label confirms, then both settle back, so you know the clipboard worked.

  • CSS
  • Buttons
  • Click
  • Added
bun add gsap

AI prompt

Create a copy-to-clipboard button for a code snippet in React and CSS. On click, call navigator.clipboard.writeText inside a try/catch, then set a "copied" state for 1.6s. Stack a copy icon and a check icon in the same grid cell. In the copied state, the copy icon scales to 0.4 and fades out while the check scales from 0.4 with a slight overshoot (cubic-bezier(0.34, 1.56, 0.64, 1), 260ms) and draws its stroke with stroke-dashoffset. Swap the label from "Copy" to "Copied" and announce it with aria-live="polite". Clear the timeout on unmount. Under prefers-reduced-motion, swap the icons instantly. The snippet reads "bun add gsap".

Source

Demo.tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import s from './demo.module.css'

export default function CopyFeedback({ labels }: { labels: Record<string, string> }) {
  const [copied, setCopied] = useState(false)
  const timer = useRef(0)

  useEffect(() => () => clearTimeout(timer.current), [])

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(labels.code)
    } catch {
      // Clipboard can be blocked (iframe, permissions); still show the feedback for the demo.
    }
    setCopied(true)
    clearTimeout(timer.current)
    timer.current = window.setTimeout(() => setCopied(false), 1600)
  }

  return (
    <div className={s.snippet}>
      <code className={s.code}>{labels.code}</code>
      <button type="button" className={s.button} data-copied={copied || undefined} onClick={copy}>
        <span className={s.icons} aria-hidden="true">
          <svg className={s.copyIcon} viewBox="0 0 24 24">
            <rect x="8" y="8" width="12" height="12" rx="2" />
            <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
          </svg>
          <svg className={s.checkIcon} viewBox="0 0 24 24">
            <path d="M5 12.5l4.5 4.5L19 7.5" />
          </svg>
        </span>
        <span aria-live="polite">{copied ? labels.copied : labels.copy}</span>
      </button>
    </div>
  )
}
demo.module.css
.snippet {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 8px 8px 16px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
}

.code {
  font: 500 var(--text-sm) / 1 var(--font-mono);
  color: var(--stage-ink);
  white-space: nowrap;
}

.button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 96px;
  height: 34px;
  padding: 0 12px;
  border: 0;
  border-radius: 8px;
  background: var(--stage-bg);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.icons { display: grid; width: 16px; height: 16px; }

.icons svg {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 180ms;
}

.checkIcon {
  stroke: var(--stage-accent) !important;
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  transform: scale(0.4);
  opacity: 0;
}

.button[data-copied] .copyIcon { transform: scale(0.4); opacity: 0; }

.button[data-copied] .checkIcon {
  transform: scale(1);
  opacity: 1;
  stroke-dashoffset: 0;
  transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 180ms, stroke-dashoffset 320ms var(--ease-out) 60ms;
}

@media (prefers-reduced-motion: reduce) {
  .icons svg, .button[data-copied] .checkIcon { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.