复制反馈

复制图标弹成一个对勾,文字随之确认,然后两者恢复原样,让你知道剪贴板已经生效。

  • CSS
  • 按钮
  • 点击
  • 收录于
bun add gsap

AI 提示词

用 React 和 CSS 为代码片段制作复制到剪贴板的按钮。点击时在 try/catch 中调用 navigator.clipboard.writeText,然后把“已复制”状态保持 1.6s。复制图标和对勾图标叠放在同一个 grid 单元格中。已复制状态下,复制图标缩放到 0.4 并淡出,对勾从 0.4 带一点回弹地放大(cubic-bezier(0.34, 1.56, 0.64, 1),260ms),同时用 stroke-dashoffset 画出描边。按钮文字从“复制”换成“已复制”,并用 aria-live="polite" 播报。组件卸载时清除 timeout。在 prefers-reduced-motion 下直接切换图标。代码片段为“bun add gsap”。

源代码

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; }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。