Clipboard Copy Tool with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #17

Project Overview

A tiny, practical Clipboard Copy Tool that lets users copy text (plain text, code blocks, emails, or form values) to the clipboard with one click. It demonstrates the modern Clipboard API (navigator.clipboard.writeText) with a graceful fallback for older browsers, plus accessible UI feedback and minimal styling.

Key Features

  • Copy text from: textareas, inputs, code blocks, or any element.
  • Uses navigator.clipboard.writeText when available, with document.execCommand('copy') fallback.
  • Visual feedback (button text, temporary tooltip/class) and accessible aria-live status messages.
  • Copy-all button to copy combined fields.
  • Keyboard-friendly buttons and focus states.
  • Tiny, dependency-free JS — drop into any project.

HTML Code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 17 — Clipboard Copy Tool</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div class="logo">CC</div>
      <div>
        <h1 id="title">Day 17: Clipboard Copy Tool</h1>
        <p class="lead">Click a button to copy text to the clipboard using the Clipboard API (with a fallback).</p>
      </div>
    </header>

    <section class="panel">
      <label class="field">
        <span class="label">Short text</span>
        <div class="row">
          <input id="shortText" type="text" value="Hello, copy me!" />
          <button class="btn copy-btn" data-copy-target="#shortText">Copy</button>
        </div>
      </label>

      <label class="field">
        <span class="label">Email</span>
        <div class="row">
          <input id="email" type="email" value="user@example.com" />
          <button class="btn copy-btn" data-copy-target="#email">Copy</button>
        </div>
      </label>

      <label class="field">
        <span class="label">Multi-line / message</span>
        <div class="row">
          <textarea id="message" rows="3">This is a longer message you can copy to clipboard.</textarea>
          <button class="btn copy-btn" data-copy-target="#message">Copy</button>
        </div>
      </label>

      <label class="field">
        <span class="label">Code snippet</span>
        <pre class="code-block" id="codeBlock" tabindex="0"><code>const sum = (a, b) => a + b;</code></pre>
        <div style="display:flex; gap:8px; margin-top:8px;">
          <button class="btn copy-btn" data-copy-target="#codeBlock">Copy code</button>
          <button class="btn" id="copyAll">Copy All Fields</button>
        </div>
      </label>

      <div id="status" class="muted" aria-live="polite">Ready to copy.</div>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

CSS Code

:root {
  --bg: #071026;
  --card: #0b1220;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: #002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(760px, 96%);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 12px;
  align-items: center;
}

.logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--accent), #22c1c3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px;
}

.panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 12px
}

.field {
  display: flex;
  flex-direction: column;
  gap: 8px
}

.label {
  font-size: 13px;
  color: var(--muted)
}

.row {
  display: flex;
  gap: 8px;
  align-items: center
}

input[type="text"],
input[type="email"],
textarea {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
  outline: none;
  min-width: 0;
}

.code-block {
  background: rgba(255, 255, 255, 0.02);
  border-radius: 8px;
  padding: 12px;
  border: 1px solid rgba(255, 255, 255, 0.03);
  overflow: auto;
  font-family: ui-monospace, monospace;
}

.btn {
  padding: 8px 12px;
  border-radius: 8px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer;
}

.copy-btn.copied {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted);
  animation: pop 220ms ease;
}

@keyframes pop {
  from {
    transform: scale(0.98);
    opacity: 0.7
  }

  to {
    transform: scale(1);
    opacity: 1
  }
}

.muted {
  color: var(--muted);
  font-size: 13px
}

Javascript Code

// Day 17 — Clipboard Copy Tool
// Uses navigator.clipboard when available, with document.execCommand fallback.

// DOM
const statusEl = document.getElementById('status');
const copyButtons = Array.from(document.querySelectorAll('.copy-btn'));
const copyAllBtn = document.getElementById('copyAll');

// Helper: read text from an element (input/textarea/code/block)
function readTextFromTarget(selectorOrEl) {
  const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl;
  if (!el) return '';
  // inputs & textareas
  if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') return el.value;
  // code / pre / div — use textContent
  return el.textContent || el.innerText || '';
}

// Primary copy function using clipboard API with fallback
async function copyText(text) {
  if (!text && text !== '') {
    return { ok: false, message: 'Nothing to copy' };
  }

  // Try modern API first
  if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
    try {
      await navigator.clipboard.writeText(text);
      return { ok: true };
    } catch (err) {
      // fallthrough to fallback
      console.warn('Clipboard API failed, falling back to legacy method', err);
    }
  }

  // Fallback approach: create a temporary textarea, select, execCommand
  try {
    const ta = document.createElement('textarea');
    ta.value = text;
    // avoid flash on screen
    ta.style.position = 'fixed';
    ta.style.left = '-9999px';
    ta.style.top = '0';
    document.body.appendChild(ta);
    ta.focus();
    ta.select();

    const successful = document.execCommand('copy');
    document.body.removeChild(ta);
    if (successful) return { ok: true };
    return { ok: false, message: 'Copy command unsuccessful' };
  } catch (err) {
    console.error('Fallback copy failed', err);
    return { ok: false, message: err && err.message ? err.message : 'Copy failed' };
  }
}

// UI feedback helper
function showStatus(msg, success = true) {
  statusEl.textContent = msg;
  statusEl.style.color = success ? '' : '#ffbaba';
}

// Button feedback: temporary change text + class
function indicateButtonCopied(btn) {
  const original = btn.textContent;
  btn.classList.add('copied');
  btn.textContent = 'Copied!';
  setTimeout(() => {
    btn.classList.remove('copied');
    btn.textContent = original;
  }, 1200);
}

// Attach per-button handlers
copyButtons.forEach(btn => {
  btn.addEventListener('click', async (e) => {
    e.preventDefault();
    const target = btn.dataset.copyTarget;
    if (!target) {
      showStatus('No target found to copy.', false);
      return;
    }
    const text = readTextFromTarget(target);
    if (!text) {
      showStatus('Nothing to copy.', false);
      return;
    }
    const res = await copyText(text);
    if (res.ok) {
      indicateButtonCopied(btn);
      showStatus('Copied to clipboard.');
    } else {
      showStatus('Copy failed: ' + (res.message || 'unknown error'), false);
    }
  });
});

// Copy all fields example: combines shortText + email + message + code
copyAllBtn.addEventListener('click', async (e) => {
  e.preventDefault();
  const parts = [];
  const shortText = readTextFromTarget('#shortText');
  const email = readTextFromTarget('#email');
  const message = readTextFromTarget('#message');
  const code = readTextFromTarget('#codeBlock');

  if (shortText) parts.push(shortText);
  if (email) parts.push(email);
  if (message) parts.push(message);
  if (code) parts.push('Code:\n' + code);

  const combined = parts.join('\n\n');
  if (!combined) {
    showStatus('Nothing to copy.', false);
    return;
  }

  const res = await copyText(combined);
  if (res.ok) {
    showStatus('All fields copied to clipboard.');
    // visual feedback on the copyAll button
    copyAllBtn.textContent = 'Copied!';
    setTimeout(() => (copyAllBtn.textContent = 'Copy All Fields'), 1200);
  } else {
    showStatus('Copy failed: ' + (res.message || 'unknown error'), false);
  }
});

// Accessibility: allow Enter/Space to trigger copy when button focused (native)
copyButtons.forEach(b => {
  b.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      b.click();
    }
  });
});

// Optional: show support notice if Clipboard API not available
(function checkSupport(){
  if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
    showStatus('Clipboard API unavailable — using fallback copy method.');
  }
})();
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 15 : Light/Dark Mode Toggle

Switch between light and dark themes with one click (and remember the choice).

Concepts: CSS variables, LocalStorage.

Day 19 : Product Filter List

Filter products or items by search or category in real-time.

Concepts: Array filter(), DOM rendering.

Day 20 : Music Player App

A mini music player with play, pause, next, and progress bar.

Concepts: Audio API, event listeners, state management.