Skip to main content

How to Handle Change, Input, and Clipboard Events in JavaScript

Forms are the primary way users interact with web applications, and reacting to user input in real time is essential for building responsive interfaces. JavaScript provides several events for tracking how form values change: change fires when a user commits a new value, input fires on every keystroke and modification, and clipboard events (cut, copy, paste) let you control how data moves in and out of form fields.

Understanding the differences between these events, when each one fires, and how to use them correctly is critical for building features like real-time validation, search-as-you-type, formatted inputs, and controlled paste behavior. This guide covers each event in detail, explains event.clipboardData for intercepting clipboard content, and introduces the modern Clipboard API for programmatic clipboard access.

change: Fires After Value Change and Loss of Focus

The change event fires when a form element's value has been modified and the element loses focus. This "commit" behavior makes it fundamentally different from the input event, which fires immediately on every change.

Text Inputs and Textareas

For <input type="text"> and <textarea>, change fires only after the user finishes editing and moves focus away from the element (clicks elsewhere, presses Tab, etc.):

<input id="name" type="text" placeholder="Type your name" />
<p id="output"></p>

<script>
const nameInput = document.getElementById('name');
const output = document.getElementById('output');

nameInput.addEventListener('change', (event) => {
output.textContent = `Changed to: ${event.target.value}`;
});
</script>

If the user types "Alice" and then clicks outside the input, the change event fires once with the value "Alice". While the user is typing, nothing happens. No event fires for individual keystrokes.

User types "A"       → no change event
User types "l" → no change event
User types "i" → no change event
User types "c" → no change event
User types "e" → no change event
User clicks outside → change event fires: "Alice"
note

The change event fires only if the value actually changed. If a user focuses the input, types nothing (or types and then deletes back to the original value), and then blurs, no change event fires.

Select Elements

For <select>, the change event fires immediately when a new option is selected, without waiting for blur. This makes sense because selecting an option is already a "commit" action:

<select id="color">
<option value="">Choose a color</option>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>

<script>
document.getElementById('color').addEventListener('change', (event) => {
console.log(`Selected: ${event.target.value}`);
});
</script>

Selecting "Green" immediately logs:

Selected: green

Checkboxes and Radio Buttons

For <input type="checkbox"> and <input type="radio">, change fires immediately when the checked state changes:

<label>
<input type="checkbox" id="agree" /> I agree to the terms
</label>

<script>
document.getElementById('agree').addEventListener('change', (event) => {
console.log(`Checked: ${event.target.checked}`);
});
</script>

Clicking the checkbox immediately logs:

Checked: true

Clicking it again:

Checked: false

Multiple Checkboxes with Delegation

When you have a group of checkboxes, event delegation with change works cleanly:

<div id="toppings">
<label><input type="checkbox" value="cheese" /> Cheese</label>
<label><input type="checkbox" value="pepperoni" /> Pepperoni</label>
<label><input type="checkbox" value="mushrooms" /> Mushrooms</label>
<label><input type="checkbox" value="olives" /> Olives</label>
</div>
<p id="selected-toppings"></p>

<script>
const toppingsDiv = document.getElementById('toppings');
const display = document.getElementById('selected-toppings');

toppingsDiv.addEventListener('change', () => {
const checked = toppingsDiv.querySelectorAll('input:checked');
const values = [...checked].map(cb => cb.value);
display.textContent = `Selected: ${values.join(', ') || 'none'}`;
});
</script>

File Inputs

For <input type="file">, change fires when the user selects a file (or cancels a previous selection by selecting a different file):

<input type="file" id="upload" accept="image/*" />

<script>
document.getElementById('upload').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
console.log(`File: ${file.name}, Size: ${file.size} bytes`);
}
});
</script>

Range Inputs

For <input type="range">, browser behavior varies. In most modern browsers, change fires when the user releases the slider handle (on mouseup/pointerup). If you need real-time tracking as the slider moves, use the input event instead.

<input type="range" id="volume" min="0" max="100" value="50" />
<span id="volume-display">50</span>

<script>
const slider = document.getElementById('volume');
const display = document.getElementById('volume-display');

// Fires only when the user releases the slider
slider.addEventListener('change', (event) => {
console.log(`Final value: ${event.target.value}`);
});

// Fires continuously as the slider moves
slider.addEventListener('input', (event) => {
display.textContent = event.target.value;
});
</script>

Summary: When change Fires

ElementWhen change fires
<input type="text">, <textarea>On blur, if value changed
<select>Immediately on selection
<input type="checkbox">Immediately on check/uncheck
<input type="radio">Immediately on selection
<input type="file">Immediately on file selection
<input type="range">On release of the slider
<input type="date">, <input type="color">On value commit (picker close)

input: Fires on Every Value Change (Real-Time)

The input event fires every time the value of a form element changes, without waiting for focus loss. It is the go-to event for real-time reactions to user input.

Basic Usage

<input id="search" type="text" placeholder="Search..." />
<div id="results"></div>

<script>
const searchInput = document.getElementById('search');
const results = document.getElementById('results');

searchInput.addEventListener('input', (event) => {
const query = event.target.value;
results.textContent = `Searching for: "${query}"`;
});
</script>

Typing "hello":

Searching for: "h"
Searching for: "he"
Searching for: "hel"
Searching for: "hell"
Searching for: "hello"

Every keystroke, paste, autofill, or any other modification triggers the event immediately.

What Triggers the input Event

The input event is not limited to keyboard input. It fires for any value change:

  • Typing characters
  • Deleting characters (Backspace, Delete)
  • Pasting content (Ctrl+V, right-click paste)
  • Cutting content (Ctrl+X)
  • Drag-and-drop text into the field
  • Browser autofill and autocomplete
  • Speech-to-text input
  • Undo/Redo (Ctrl+Z, Ctrl+Y)

This is what makes input more reliable than keyboard events for tracking value changes. Keyboard events miss paste, autofill, and other non-keyboard modifications.

// ❌ Misses paste, autofill, drag-and-drop
input.addEventListener('keydown', () => {
console.log('Value:', input.value); // Value hasn't changed yet on keydown!
});

// ✅ Catches every value change from any source
input.addEventListener('input', () => {
console.log('Value:', input.value); // Always the current value
});

The InputEvent Properties

The input event provides an InputEvent object with useful properties:

input.addEventListener('input', (event) => {
console.log(event.data); // The inserted character(s), or null for deletion
console.log(event.inputType); // What kind of input occurred
console.log(event.isComposing); // Is an IME composition in progress?
});

The inputType property describes the type of change:

inputTypeDescription
"insertText"Character typed normally
"insertFromPaste"Content pasted
"insertFromDrop"Content dropped via drag-and-drop
"deleteContentBackward"Backspace pressed
"deleteContentForward"Delete pressed
"deleteByCut"Content cut
"insertReplacementText"Autocomplete/autofill
"historyUndo"Undo (Ctrl+Z)
"historyRedo"Redo (Ctrl+Y)
input.addEventListener('input', (event) => {
switch (event.inputType) {
case 'insertFromPaste':
console.log('User pasted content');
break;
case 'deleteByCut':
console.log('User cut content');
break;
case 'insertReplacementText':
console.log('Autofill or autocomplete triggered');
break;
}
});

Real-Time Validation

A common pattern is validating input as the user types:

<label>
Email: <input id="email" type="text" />
<span id="email-status"></span>
</label>

<script>
const emailInput = document.getElementById('email');
const status = document.getElementById('email-status');

emailInput.addEventListener('input', () => {
const value = emailInput.value;

if (value === '') {
status.textContent = '';
status.style.color = '';
} else if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
status.textContent = '✓ Valid email';
status.style.color = 'green';
} else {
status.textContent = '✗ Invalid email';
status.style.color = 'red';
}
});
</script>

Character Counter

<textarea id="bio" maxlength="200" placeholder="Write your bio..."></textarea>
<div id="counter">0 / 200</div>

<script>
const bio = document.getElementById('bio');
const counter = document.getElementById('counter');

bio.addEventListener('input', () => {
const length = bio.value.length;
counter.textContent = `${length} / 200`;

if (length > 180) {
counter.style.color = 'red';
} else if (length > 150) {
counter.style.color = 'orange';
} else {
counter.style.color = '';
}
});
</script>

Debouncing the input Event

For operations like API calls or expensive search queries, you should not fire on every keystroke. Debounce the handler to wait until the user pauses typing:

function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}

const searchInput = document.getElementById('search');

searchInput.addEventListener('input', debounce(async (event) => {
const query = event.target.value.trim();
if (query.length < 2) return;

const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await results.json();
displayResults(data);
}, 300)); // Wait 300ms after user stops typing

input on Non-Text Elements

The input event also works on <select>, <input type="range">, <input type="checkbox">, and other form elements. For range sliders, input is particularly useful because it fires continuously during sliding, while change fires only on release:

const slider = document.getElementById('opacity');

// Real-time updates as the slider moves
slider.addEventListener('input', () => {
preview.style.opacity = slider.value / 100;
});

input vs. change: The Key Differences

Featureinputchange
When it fires (text fields)Every modificationOn blur, if value changed
When it fires (select/checkbox)On every changeOn every change
Catches pasteYesYes (on blur)
Catches autofillYesYes (on blur)
Use caseReal-time feedbackFinal value submission
Can be preventedNo (preventDefault has no effect)No
tip

Use input when you need to react as the user types (search, validation, character counting, formatting). Use change when you only care about the final committed value (saving settings, form submission logic).

The input Event Cannot Be Prevented

Unlike keyboard events, calling event.preventDefault() on the input event does not prevent the value from changing. The value has already changed by the time input fires:

// ❌ This does NOT prevent the character from being inserted
input.addEventListener('input', (event) => {
event.preventDefault(); // Has no effect
});

// ✅ To prevent characters, use keydown instead
input.addEventListener('keydown', (event) => {
if (!/\d/.test(event.key) && event.key.length === 1) {
event.preventDefault(); // Blocks the character from being typed
}
});

If you need to transform or filter the value, modify input.value directly inside the input handler:

// Force uppercase input
input.addEventListener('input', () => {
const pos = input.selectionStart; // Save cursor position
input.value = input.value.toUpperCase();
input.setSelectionRange(pos, pos); // Restore cursor position
});

cut, copy, paste: Clipboard Events

The clipboard events fire when the user cuts, copies, or pastes content. These events fire on the element that has focus and can be prevented to block the clipboard operation.

Basic Clipboard Event Handling

const input = document.getElementById('text-input');

input.addEventListener('copy', (event) => {
console.log('User is copying');
});

input.addEventListener('cut', (event) => {
console.log('User is cutting');
});

input.addEventListener('paste', (event) => {
console.log('User is pasting');
});

Event Order

When the user pastes content into an input, the events fire in this order:

  1. paste event fires (content not yet inserted)
  2. The content is inserted into the input
  3. input event fires (content now inserted)

For cut operations:

  1. cut event fires (content not yet removed)
  2. The content is removed from the input
  3. input event fires (content now removed)

Preventing Clipboard Operations

You can prevent clipboard operations by calling preventDefault():

// Prevent copying (e.g., for content protection, easily bypassed though)
document.addEventListener('copy', (event) => {
event.preventDefault();
console.log('Copying is disabled on this page');
});

// Prevent pasting into a specific field
passwordConfirm.addEventListener('paste', (event) => {
event.preventDefault();
showMessage('Please type your password manually');
});
warning

Preventing paste on password confirmation fields is a controversial practice. It harms users who use password managers and does not improve security. Many accessibility guidelines recommend against disabling paste.

Modifying Clipboard Content on Copy

A more practical use case is modifying what gets copied. For example, adding a source attribution when users copy text from your site:

document.addEventListener('copy', (event) => {
const selection = document.getSelection().toString();

if (selection.length > 50) {
const modifiedText = `${selection}\n\nSource: ${window.location.href}`;
event.clipboardData.setData('text/plain', modifiedText);
event.preventDefault(); // Use our modified text instead of the default
}
});

event.clipboardData

The clipboardData property on clipboard events provides access to the data being copied, cut, or pasted. It is a DataTransfer object with methods for reading and writing clipboard content in different formats.

Reading Pasted Content

On the paste event, you can read what the user is pasting before it enters the input:

input.addEventListener('paste', (event) => {
const pastedText = event.clipboardData.getData('text/plain');
console.log(`Pasting: "${pastedText}"`);
});

The getData() method accepts a MIME type:

input.addEventListener('paste', (event) => {
// Plain text
const text = event.clipboardData.getData('text/plain');

// HTML content (if the user copies from a web page)
const html = event.clipboardData.getData('text/html');

// URL (if the user copies a link)
const url = event.clipboardData.getData('text/uri-list');

console.log('Plain text:', text);
console.log('HTML:', html);
console.log('URL:', url);
});

Writing to the Clipboard on Copy/Cut

On copy and cut events, you can replace the clipboard content:

document.addEventListener('copy', (event) => {
const selection = document.getSelection().toString();

// Set both plain text and HTML versions
event.clipboardData.setData('text/plain', selection.toUpperCase());
event.clipboardData.setData('text/html', `<strong>${selection.toUpperCase()}</strong>`);

event.preventDefault(); // Required to use our custom data
});
note

You must call event.preventDefault() when using setData() on copy or cut events. Without it, the browser overwrites your custom clipboard data with the default selection.

Filtering Pasted Content

A practical use case is sanitizing pasted content. For example, stripping formatting and allowing only plain text in a rich text area:

editableDiv.addEventListener('paste', (event) => {
event.preventDefault();

// Get plain text only, stripping all HTML formatting
const text = event.clipboardData.getData('text/plain');

// Insert as plain text
document.execCommand('insertText', false, text);
});

Or filtering pasted content in a phone number field:

phoneInput.addEventListener('paste', (event) => {
event.preventDefault();

const pastedText = event.clipboardData.getData('text/plain');

// Extract only digits from pasted content
const digitsOnly = pastedText.replace(/\D/g, '');

// Insert the cleaned text at the cursor position
const start = phoneInput.selectionStart;
const end = phoneInput.selectionEnd;
const currentValue = phoneInput.value;

phoneInput.value = currentValue.slice(0, start) + digitsOnly + currentValue.slice(end);

// Place cursor after the inserted text
const newPos = start + digitsOnly.length;
phoneInput.setSelectionRange(newPos, newPos);

// Manually fire the input event since we prevented default
phoneInput.dispatchEvent(new Event('input', { bubbles: true }));
});

Pasting Files and Images

The clipboardData object can also contain files. This is common when users paste screenshots:

document.addEventListener('paste', (event) => {
const items = event.clipboardData.items;

for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
console.log(`Pasted image: ${file.type}, ${file.size} bytes`);

// Preview the pasted image
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.src = url;
img.style.maxWidth = '300px';
document.getElementById('preview').appendChild(img);

event.preventDefault();
}
}
});

clipboardData.items vs. clipboardData.files

The items property provides a DataTransferItemList with all clipboard items, while files is a FileList containing only file items:

document.addEventListener('paste', (event) => {
// All items (text, HTML, files)
console.log('Items:', event.clipboardData.items.length);
for (const item of event.clipboardData.items) {
console.log(` Kind: ${item.kind}, Type: ${item.type}`);
// kind is "string" or "file"
}

// Only files
console.log('Files:', event.clipboardData.files.length);
for (const file of event.clipboardData.files) {
console.log(` File: ${file.name}, Type: ${file.type}`);
}
});

The Clipboard API (navigator.clipboard)

The event.clipboardData approach works within event handlers, but sometimes you need to read from or write to the clipboard programmatically, outside of a clipboard event. The modern Clipboard API provides this capability through navigator.clipboard.

Writing Text to the Clipboard

async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Copied to clipboard!');
} catch (err) {
console.error('Failed to copy:', err);
}
}

// Usage: a "Copy" button
const copyButton = document.getElementById('copy-btn');
const codeBlock = document.getElementById('code');

copyButton.addEventListener('click', async () => {
await copyToClipboard(codeBlock.textContent);
copyButton.textContent = 'Copied!';
setTimeout(() => {
copyButton.textContent = 'Copy';
}, 2000);
});

Reading Text from the Clipboard

async function readFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('Clipboard contains:', text);
return text;
} catch (err) {
console.error('Failed to read clipboard:', err);
return null;
}
}

// Usage: a "Paste" button
document.getElementById('paste-btn').addEventListener('click', async () => {
const text = await readFromClipboard();
if (text) {
document.getElementById('output').textContent = text;
}
});

Writing Rich Content (HTML, Images)

The write() method supports writing multiple formats and binary data using ClipboardItem:

async function copyRichContent() {
const htmlContent = '<strong>Bold text</strong> and <em>italic text</em>';
const plainContent = 'Bold text and italic text';

const clipboardItem = new ClipboardItem({
'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([plainContent], { type: 'text/plain' })
});

try {
await navigator.clipboard.write([clipboardItem]);
console.log('Rich content copied!');
} catch (err) {
console.error('Failed to copy:', err);
}
}

Copying an image to the clipboard:

async function copyImage(imageUrl) {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();

const clipboardItem = new ClipboardItem({
[blob.type]: blob
});

await navigator.clipboard.write([clipboardItem]);
console.log('Image copied to clipboard!');
} catch (err) {
console.error('Failed to copy image:', err);
}
}

Reading Rich Content (Images, Files)

async function readClipboardContent() {
try {
const items = await navigator.clipboard.read();

for (const item of items) {
console.log('Available types:', item.types);

for (const type of item.types) {
const blob = await item.getType(type);

if (type.startsWith('image/')) {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.getElementById('preview').appendChild(img);
} else if (type === 'text/plain') {
const text = await blob.text();
console.log('Text:', text);
} else if (type === 'text/html') {
const html = await blob.text();
console.log('HTML:', html);
}
}
}
} catch (err) {
console.error('Failed to read clipboard:', err);
}
}

Permission and Security Requirements

The Clipboard API has strict security requirements:

  1. HTTPS required: The Clipboard API only works on secure contexts (HTTPS pages or localhost).

  2. User gesture required: Most browsers require that clipboard operations are triggered by a user action (click, keypress). You cannot read from or write to the clipboard during page load.

  3. Permission required for reading: Reading from the clipboard requires the user to grant the "clipboard-read" permission. The browser typically shows a permission prompt.

// Check for clipboard API support
if (!navigator.clipboard) {
console.log('Clipboard API not available (requires HTTPS)');
}

// Check permissions
async function checkClipboardPermission() {
try {
const permission = await navigator.permissions.query({
name: 'clipboard-read'
});
console.log(`Clipboard read permission: ${permission.state}`);
// "granted", "denied", or "prompt"
} catch (err) {
console.log('Permission query not supported');
}
}
tip

Writing to the clipboard (writeText, write) generally does not require an explicit permission prompt. It only requires a user gesture (like a button click). Reading from the clipboard (readText, read) requires both a user gesture and explicit permission from the user.

Fallback for Older Browsers

Not all browsers fully support the Clipboard API. Here is a utility with fallbacks:

async function copyText(text) {
// Modern Clipboard API
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// Fall through to legacy approach
}
}

// Legacy fallback using execCommand
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
document.body.appendChild(textarea);
textarea.select();

try {
const success = document.execCommand('copy');
return success;
} catch (err) {
console.error('Fallback copy failed:', err);
return false;
} finally {
document.body.removeChild(textarea);
}
}
warning

The document.execCommand('copy') fallback is deprecated but still works in all browsers. It requires creating a temporary textarea, selecting its content, and executing the copy command. Use the Clipboard API as the primary approach and execCommand only as a fallback.

Clipboard API vs. Clipboard Events Comparison

FeatureClipboard Events (event.clipboardData)Clipboard API (navigator.clipboard)
TriggerUser-initiated cut/copy/pasteProgrammatic (button click, etc.)
Read clipboardOnly during paste eventAnytime (with permission)
Write clipboardOnly during copy/cut eventAnytime (with user gesture)
Modify default behaviorYes (preventDefault + setData)N/A (separate operation)
AsyncNo (synchronous)Yes (returns Promise)
File/image supportVia clipboardData.itemsVia ClipboardItem
HTTPS requiredNoYes
Permission promptNoFor reading, yes

Practical Example: Smart Input Formatter

Here is a complete example combining multiple events to build a credit card number input that formats as you type, handles paste, and validates in real time:

<style>
.card-input {
font-family: monospace;
font-size: 18px;
padding: 10px 14px;
border: 2px solid #ccc;
border-radius: 8px;
width: 240px;
letter-spacing: 2px;
transition: border-color 0.2s;
}
.card-input.valid { border-color: #2ecc71; }
.card-input.invalid { border-color: #e74c3c; }
.card-type { margin-top: 8px; font-size: 14px; color: #666; }
</style>

<input
id="card-number"
class="card-input"
type="text"
placeholder="0000 0000 0000 0000"
maxlength="19"
autocomplete="cc-number"
/>
<div class="card-type" id="card-type"></div>

<script>
const cardInput = document.getElementById('card-number');
const cardType = document.getElementById('card-type');

function formatCardNumber(value) {
// Remove all non-digits
const digits = value.replace(/\D/g, '');
// Group into chunks of 4
const groups = digits.match(/.{1,4}/g);
return groups ? groups.join(' ') : '';
}

function detectCardType(digits) {
if (/^4/.test(digits)) return 'Visa';
if (/^5[1-5]/.test(digits)) return 'Mastercard';
if (/^3[47]/.test(digits)) return 'American Express';
if (/^6(?:011|5)/.test(digits)) return 'Discover';
return '';
}

// Handle real-time input (typing, deleting)
cardInput.addEventListener('input', (event) => {
const cursorPos = cardInput.selectionStart;
const oldValue = cardInput.value;
const newValue = formatCardNumber(oldValue);

cardInput.value = newValue;

// Adjust cursor position for inserted spaces
const spacesBeforeCursor = (newValue.slice(0, cursorPos).match(/ /g) || []).length;
const oldSpaces = (oldValue.slice(0, cursorPos).match(/ /g) || []).length;
const adjustedPos = cursorPos + (spacesBeforeCursor - oldSpaces);
cardInput.setSelectionRange(adjustedPos, adjustedPos);

// Detect card type
const digits = newValue.replace(/\D/g, '');
const type = detectCardType(digits);
cardType.textContent = type ? `Card type: ${type}` : '';

// Visual validation
cardInput.classList.remove('valid', 'invalid');
if (digits.length === 16) {
cardInput.classList.add('valid');
} else if (digits.length > 0) {
cardInput.classList.add('invalid');
}
});

// Handle paste: clean and format pasted content
cardInput.addEventListener('paste', (event) => {
event.preventDefault();

const pastedText = event.clipboardData.getData('text/plain');
const digits = pastedText.replace(/\D/g, '').slice(0, 16);
const formatted = formatCardNumber(digits);

cardInput.value = formatted;
cardInput.dispatchEvent(new Event('input', { bubbles: true }));
});

// Log final value on commit
cardInput.addEventListener('change', () => {
const digits = cardInput.value.replace(/\D/g, '');
console.log(`Card number committed: ${digits}`);
});
</script>

This example demonstrates all three event types working together: input for real-time formatting and validation, paste for cleaning pasted content, and change for final value handling.

Summary

JavaScript provides distinct events for different stages of form value changes:

  • change fires when the user commits a new value. For text inputs and textareas, this means after blur. For selects, checkboxes, and radio buttons, it fires immediately. Use it when you care about the final value, not intermediate states.
  • input fires on every value modification in real time, regardless of how the change occurred (typing, pasting, autofill, speech input, undo). Use it for search-as-you-type, live validation, character counters, and input formatting. It cannot be prevented with preventDefault().
  • Clipboard events (cut, copy, paste) fire during clipboard operations. Use event.clipboardData with getData() to read pasted content and setData() with preventDefault() to modify copied/cut content. These work synchronously within event handlers.
  • The Clipboard API (navigator.clipboard) provides programmatic, asynchronous clipboard access outside of event handlers. It requires HTTPS and user gestures. writeText() works with a user gesture, while readText() also requires explicit permission.

For most form interactions, input is the event you will reach for most often. Combine it with change for commit-time logic and clipboard events when you need to control or inspect what moves through the clipboard.