Tailwind + JSMIT licensed

Tailwind Blockquote with One-Click Copy Button

A quote card with a copy button that fades in on hover or focus, flips to a green “Copied” state, and announces success through a polite live region. The port drives the copied state with a data-copied attribute and v4's bare data-copied: variant, keeps the button always visible on touch (pointer-coarse:), and adds an execCommand fallback for iframes where the Clipboard API is blocked.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tbq-25 w-full min-h-svh grid place-items-center bg-tbq25-bg bg-[radial-gradient(75%_60%_at_50%_0%,var(--color-tbq25-glow),transparent)] px-5 py-14 font-tbq25-sans text-tbq25-ink" aria-label="Blockquote with one-click copy button demo">
  <div class="grid w-full justify-items-center gap-7">
    <figure data-tbq25-card class="group relative w-full max-w-[540px] rounded-[18px] border border-white/10 bg-tbq25-card p-[clamp(26px,4vw,36px)] shadow-[0_22px_54px_oklch(0_0_0/0.4)]">
      <div class="mb-3 flex items-start justify-between gap-3">
        <span aria-hidden="true" class="h-9 font-tbq25-serif text-[64px] leading-[0.9] text-tbq25-acc">“</span>
        <button type="button" data-copy class="group/btn inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-[10px] border border-white/16 bg-white/6 px-3.5 text-[12.5px] font-semibold text-tbq25-quiet opacity-0 transition-[opacity,background-color,border-color,color] duration-200 group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-white/12 hover:text-tbq25-ink focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tbq25-acc pointer-coarse:opacity-100 data-copied:border-tbq25-ok/55 data-copied:bg-tbq25-ok/16 data-copied:text-tbq25-ok data-copied:opacity-100">
          <svg class="group-data-copied/btn:hidden" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="5.2" y="5.2" width="8.2" height="8.2" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.8 5.2V4a1.6 1.6 0 0 0-1.6-1.6H4A1.6 1.6 0 0 0 2.4 4v5.2A1.6 1.6 0 0 0 4 10.8h1.2" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
          <svg class="hidden group-data-copied/btn:block" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M2.8 8.6l3.4 3.4 7-7.6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>
          <span class="group-data-copied/btn:hidden">Copy quote</span>
          <span class="hidden group-data-copied/btn:inline">Copied</span>
        </button>
      </div>
      <blockquote>
        <p class="font-tbq25-serif text-[clamp(21px,2.9vw,27px)] leading-[1.45] italic text-tbq25-ink text-pretty">Programs must be written for people to read, and only incidentally for machines to execute.</p>
      </blockquote>
      <figcaption class="mt-4 text-[13px] text-tbq25-mut">Harold Abelson, <cite class="italic text-tbq25-quiet">Structure and Interpretation of Computer Programs</cite></figcaption>
      <span role="status" aria-live="polite" class="sr-only"></span>
    </figure>
    <p aria-hidden="true" class="rounded-full border border-white/14 px-3.5 py-2 font-tbq25-mono text-[11.5px] text-tbq25-mut">data-copied: variant · clipboard + fallback · pointer-coarse: visible</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tbq25-bg: #0f171f;
  --color-tbq25-bg: oklch(0.2 0.02 250);
  --color-tbq25-glow: #0c2a39b3;
  --color-tbq25-glow: oklch(0.27 0.045 235 / 0.7);
  --color-tbq25-card: #1a222c;
  --color-tbq25-card: oklch(0.25 0.022 252);
  --color-tbq25-ink: #eef2f7;
  --color-tbq25-ink: oklch(0.96 0.008 250);
  --color-tbq25-quiet: #c7cfd7;
  --color-tbq25-quiet: oklch(0.85 0.015 250);
  --color-tbq25-mut: #9ba6b1;
  --color-tbq25-mut: oklch(0.72 0.02 250);
  --color-tbq25-acc: #00c0d8;
  --color-tbq25-acc: oklch(0.74 0.13 210);
  --color-tbq25-ok: #61cb7c;
  --color-tbq25-ok: oklch(0.76 0.15 150);
  --font-tbq25-serif: 'Newsreader', Georgia, serif;
  --font-tbq25-sans: 'Geist', system-ui, sans-serif;
  --font-tbq25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tbq-25 {
  width: 100%;
  min-height: 100svh;
}
(() => {
  const copyText = async (text) => {
    try { await navigator.clipboard.writeText(text); return true; }
    catch {
      const ta = Object.assign(document.createElement('textarea'), { value: text });
      ta.setAttribute('readonly', '');
      ta.style.cssText = 'position:fixed;opacity:0';
      document.body.append(ta);
      ta.select();
      const ok = document.execCommand('copy');
      ta.remove();
      return ok;
    }
  };
  document.querySelectorAll('[data-tbq25-card]').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    const btn = card.querySelector('[data-copy]');
    const status = card.querySelector('[role="status"]');
    let t = 0;
    btn.addEventListener('click', async () => {
      const text = '\u201C' + card.querySelector('blockquote').textContent.trim() + '\u201D \u2014 ' + card.querySelector('figcaption').textContent.trim();
      const ok = await copyText(text);
      clearTimeout(t);
      if (ok) { btn.setAttribute('data-copied', ''); status.textContent = 'Quote copied to clipboard'; }
      else status.textContent = 'Copy failed \u2014 select the text instead';
      t = setTimeout(() => { btn.removeAttribute('data-copied'); status.textContent = ''; }, 1800);
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tbq25-bg: #0f171f;
  --color-tbq25-bg: oklch(0.2 0.02 250);
  --color-tbq25-glow: #0c2a39b3;
  --color-tbq25-glow: oklch(0.27 0.045 235 / 0.7);
  --color-tbq25-card: #1a222c;
  --color-tbq25-card: oklch(0.25 0.022 252);
  --color-tbq25-ink: #eef2f7;
  --color-tbq25-ink: oklch(0.96 0.008 250);
  --color-tbq25-quiet: #c7cfd7;
  --color-tbq25-quiet: oklch(0.85 0.015 250);
  --color-tbq25-mut: #9ba6b1;
  --color-tbq25-mut: oklch(0.72 0.02 250);
  --color-tbq25-acc: #00c0d8;
  --color-tbq25-acc: oklch(0.74 0.13 210);
  --color-tbq25-ok: #61cb7c;
  --color-tbq25-ok: oklch(0.76 0.15 150);
  --font-tbq25-serif: 'Newsreader', Georgia, serif;
  --font-tbq25-sans: 'Geist', system-ui, sans-serif;
  --font-tbq25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tbq-25 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400..700;1,6..72,400..700&family=Geist:wght@400..800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • .is-copied and its four descendant rules became a data-copied attribute read by data-copied: and group-data-copied/btn: variants — the script no longer knows any class names.
  • @media (hover:none){opacity:1} became pointer-coarse:opacity-100, v4's built-in variant for touch-first devices.
  • UI upgrade from the CSS original: the button is now 44px tall (was 36px) and sits in a header row beside a decorative quote mark, instead of absolutely positioned over the first line of the quote, where it covered text on narrow cards.
  • UI upgrade: an execCommand fallback makes Copy work inside sandboxed playground iframes where navigator.clipboard is blocked; copied text now uses typographic quotes, and muted copy rose from 0.62 to 0.72.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Blockquote with One-Click Copy Button
Source: https://codefronts.com/snippets/tailwind-css-blockquotes/tailwind-blockquote-one-click-copy-button/

A quote card with a copy button that fades in on hover or focus, flips to a green “Copied” state, and announces success through a polite live region. The port drives the copied state with a data-copied attribute and v4's bare data-copied: variant, keeps the button always visible on touch (pointer-coarse:), and adds an execCommand fallback for iframes where the Clipboard API is blocked.
## HTML
```html
<section class="tbq-25 w-full min-h-svh grid place-items-center bg-tbq25-bg bg-[radial-gradient(75%_60%_at_50%_0%,var(--color-tbq25-glow),transparent)] px-5 py-14 font-tbq25-sans text-tbq25-ink" aria-label="Blockquote with one-click copy button demo">
  <div class="grid w-full justify-items-center gap-7">
    <figure data-tbq25-card class="group relative w-full max-w-[540px] rounded-[18px] border border-white/10 bg-tbq25-card p-[clamp(26px,4vw,36px)] shadow-[0_22px_54px_oklch(0_0_0/0.4)]">
      <div class="mb-3 flex items-start justify-between gap-3">
        <span aria-hidden="true" class="h-9 font-tbq25-serif text-[64px] leading-[0.9] text-tbq25-acc">“</span>
        <button type="button" data-copy class="group/btn inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-[10px] border border-white/16 bg-white/6 px-3.5 text-[12.5px] font-semibold text-tbq25-quiet opacity-0 transition-[opacity,background-color,border-color,color] duration-200 group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-white/12 hover:text-tbq25-ink focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tbq25-acc pointer-coarse:opacity-100 data-copied:border-tbq25-ok/55 data-copied:bg-tbq25-ok/16 data-copied:text-tbq25-ok data-copied:opacity-100">
          <svg class="group-data-copied/btn:hidden" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="5.2" y="5.2" width="8.2" height="8.2" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.8 5.2V4a1.6 1.6 0 0 0-1.6-1.6H4A1.6 1.6 0 0 0 2.4 4v5.2A1.6 1.6 0 0 0 4 10.8h1.2" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
          <svg class="hidden group-data-copied/btn:block" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M2.8 8.6l3.4 3.4 7-7.6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>
          <span class="group-data-copied/btn:hidden">Copy quote</span>
          <span class="hidden group-data-copied/btn:inline">Copied</span>
        </button>
      </div>
      <blockquote>
        <p class="font-tbq25-serif text-[clamp(21px,2.9vw,27px)] leading-[1.45] italic text-tbq25-ink text-pretty">Programs must be written for people to read, and only incidentally for machines to execute.</p>
      </blockquote>
      <figcaption class="mt-4 text-[13px] text-tbq25-mut">Harold Abelson, <cite class="italic text-tbq25-quiet">Structure and Interpretation of Computer Programs</cite></figcaption>
      <span role="status" aria-live="polite" class="sr-only"></span>
    </figure>
    <p aria-hidden="true" class="rounded-full border border-white/14 px-3.5 py-2 font-tbq25-mono text-[11.5px] text-tbq25-mut">data-copied: variant · clipboard + fallback · pointer-coarse: visible</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tbq25-bg: #0f171f;
  --color-tbq25-bg: oklch(0.2 0.02 250);
  --color-tbq25-glow: #0c2a39b3;
  --color-tbq25-glow: oklch(0.27 0.045 235 / 0.7);
  --color-tbq25-card: #1a222c;
  --color-tbq25-card: oklch(0.25 0.022 252);
  --color-tbq25-ink: #eef2f7;
  --color-tbq25-ink: oklch(0.96 0.008 250);
  --color-tbq25-quiet: #c7cfd7;
  --color-tbq25-quiet: oklch(0.85 0.015 250);
  --color-tbq25-mut: #9ba6b1;
  --color-tbq25-mut: oklch(0.72 0.02 250);
  --color-tbq25-acc: #00c0d8;
  --color-tbq25-acc: oklch(0.74 0.13 210);
  --color-tbq25-ok: #61cb7c;
  --color-tbq25-ok: oklch(0.76 0.15 150);
  --font-tbq25-serif: 'Newsreader', Georgia, serif;
  --font-tbq25-sans: 'Geist', system-ui, sans-serif;
  --font-tbq25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tbq-25 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(() => {
  const copyText = async (text) => {
    try { await navigator.clipboard.writeText(text); return true; }
    catch {
      const ta = Object.assign(document.createElement('textarea'), { value: text });
      ta.setAttribute('readonly', '');
      ta.style.cssText = 'position:fixed;opacity:0';
      document.body.append(ta);
      ta.select();
      const ok = document.execCommand('copy');
      ta.remove();
      return ok;
    }
  };
  document.querySelectorAll('[data-tbq25-card]').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    const btn = card.querySelector('[data-copy]');
    const status = card.querySelector('[role="status"]');
    let t = 0;
    btn.addEventListener('click', async () => {
      const text = '\u201C' + card.querySelector('blockquote').textContent.trim() + '\u201D \u2014 ' + card.querySelector('figcaption').textContent.trim();
      const ok = await copyText(text);
      clearTimeout(t);
      if (ok) { btn.setAttribute('data-copied', ''); status.textContent = 'Quote copied to clipboard'; }
      else status.textContent = 'Copy failed \u2014 select the text instead';
      t = setTimeout(() => { btn.removeAttribute('data-copied'); status.textContent = ''; }, 1800);
    });
  });
})();
```

How this works

The script sets one attribute; utilities do the rest.

btn.setAttribute('data-copied', '');
// markup:
<button class="group/btn … data-copied:text-tbq25-ok">
  <svg class="group-data-copied/btn:hidden">…copy…</svg>
  <svg class="hidden group-data-copied/btn:block">…check…</svg>

Hidden until needed, but never unreachable. The button is transparent at rest, visible on card hover, on keyboard focus (group-focus-within:) and permanently on touch screens, where hover doesn't exist.

Clipboard can fail. Sandboxed iframes and non-HTTPS pages reject navigator.clipboard; the fallback copies via a temporary textarea, and the live region reports failure honestly if both fail.

Make it yours

  • Always visible: delete opacity-0 and the reveal variants.
  • Copy with a link: append location.href to the copied text.
  • Toast instead of button label: move the role="status" text into a visible pill.

Gotchas — read before shipping

  • Don't hide the button with invisible/hidden — it must stay in the tab order to be revealed by focus.
  • group-focus-within: reveals the button when anything in the card is focused; focus-visible:opacity-100 on the button itself covers direct focus.
  • document.execCommand('copy') is deprecated but still the only fallback in many embedded contexts; keep it as a fallback, never the primary path.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Async Clipboard writeText is Chrome 66 / Safari 13.1 / Firefox 63 on secure origins; pointer: coarse is universal.

Techniques used in this demo

Search CodeFronts

Loading…