Modern web browsers ship with a growing number of powerful, native JavaScript APIs that let developers build more dynamic, performant, and user-friendly applications — no external libraries required. In this post, we’ll explore six of these APIs: structuredClone
, EyeDropper, AbortController
, Intersection Observer, ResizeObserver
, and the Clipboard API. For each, we’ll explain what problems it solves, when to use it, and how to implement it.
Before we dive in, it’s important to know why these APIs aren’t more widely known, despite how useful they are:
structuredClone
Deep copying objects has always been difficult in JavaScript. Aside from using an external library, one of the go-to solutions was to stringify the object and then JSON parse it. Though this works for simple objects, it fails when:
undefined
, NaN
, Infinity
, etc.Enter structuredClone
, a widely supported method for deep cloning complex objects without relying on an external library. While it’s been used internally for a while, it was only made available in the public API a few years ago. Say goodbye to Lodash!
// Original object with nested data const original = { name: "Richardson", age: 28, address: { city: "New York", street: "Wall Street", zip: "10001" }, hobbies: ["reading", "hiking"], created: new Date() }; // Create a deep clone using structuredClone const copy = structuredClone(original); // Modify the clone copy.name = "Jane"; copy.address.city = "Los Angeles"; copy.hobbies.push("music"); // The original object remains unchanged console.log("Original:", original); console.log("Clone:", copy);
See how the original object remains unchanged? Also, observe that the date in the created
field is a date object that is independent of original.created
. There is no explicit conversion of string to date here — it is all taken care of by the structuredClone
function. The map against the address
field is a completely new object, and so is the array against the hobbies
key.
The EyeDropper API allows developers to build a color picker natively without relying on any component library or package. It is still experimental and available only in Chrome, Edge, and Opera. It comes in handy when building apps that allow users to edit or manipulate something, such as a photo editor or a whiteboarding or drawing application.
Below is a quick example code of how to use the EyeDropper API:
<button id="pickColor">Pick a Color</button> <div id="colorDisplay" style="width: 100px; height: 100px; border: 1px solid #ccc" ></div> <br /> <div>Pick a color</div> <br /> <div style="display: flex; flex-direction: row; height: 5rem"> <div style="background-color: brown; width: 5rem"></div> <div style="background-color: crimson; width: 5rem"></div> <div style="background-color: blueviolet; width: 5rem"></div> <div style="background-color: chartreuse; width: 5rem"></div> <div style="background-color: darkgreen; width: 5rem"></div> </div> <script> document.getElementById("pickColor").addEventListener("click", async () => { if ("EyeDropper" in window) { const eyeDropper = new EyeDropper(); try { const result = await eyeDropper.open(); document.getElementById("colorDisplay").style.backgroundColor = result.sRGBHex; } catch (e) { console.error("Error using EyeDropper:", e); } } else { alert("EyeDropper API is not supported in this browser."); } }); </script>
In this code snippet, we create a button element with ID pickColor
, and add a click listener. When clicked, we check if the browser supports the EyeDropper API. If it does, we create a new EyeDropper instance and open it.
The open
function returns a promise that resolves to a color value selected using the eye dropper tool. When the color is selected, we change the div’s style to match the selected background color. If the browser doesn’t support the EyeDropper API, we throw an alert with an error message.
EyeDropper demo
AbortController
One of the common problems when building a search UI component is handling stale requests. For example, a request is triggered when a user types a character in an input box. To prevent unnecessary requests from being triggered, we can add a debounce. But now, imagine the user starts typing again after the debounce duration or maybe navigates to a different page. In that case, the already triggered request cannot be canceled, and we might get stale results that we don’t care about. This can hamper user experience and load the API server unnecessarily.
To abort requests that are already sent, we can use AbortController
. Let’s take a look at an example of this API in action:
import React, { useEffect, useState } from 'react'; function SearchComponent({ query }) { const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; const debounceRequest = setTimeout(() => { setLoading(true); fetch(`https://api.example.com/search?q=${query}`, { signal }) .then(res => res.json()) .then(data => { setResults(data.results); setLoading(false); }) .catch(err => { if (err.name === 'AbortError') { console.log("Request was aborted") } else { console.log('Search failed', err); setLoading(false); } }); }, 500); return () => { clearTimeout(debounceRequest); controller.abort(); }; }, [query]); return ( <div> {loading && <p>Searching...</p>} <ul> {results.map(r => ( <li key={r.id}>{r.name}</li> ))} </ul> </div> ); }
There are a few things to note here. The query
prop is given as an input to the SearchComponent
, and is added to the dependency array of useEffect
Hook. So, every time the query
changes, the useEffect
Hook is called.
When the component is mounted and when the query
prop changes, the useEffect
Hook runs, and it creates an instance of AbortController
. A function with 500ms debounce is created.
After 500ms, the function is called, and it makes an API call to fetch data based on the query
passed to it. Upon receiving the response, the state is updated and displays the results in an unordered list.
Also, observe how signal
from AbortController
is passed to the fetch
call.
If the component gets unmounted (user navigates to a different route) or if the query changes (user types in a new character after 500ms but before the search results are displayed), the cleanup function is called, which clears the timeout and aborts the fetch call.
N.B., if you are using Axios with AbortController
, make sure that you are using v0.22.0 and above. The older versions don’t support AbortController
for cancelling requests.
Are you trying to lazy-load images to optimize page load speeds? Or maybe load and play videos only when they’re visible? Maybe you’re trying to implement infinite scroll? Well, you’re in luck — you don’t need any npm packages to do any of that. Enter the Intersection Observer API.
Intersection Observer is a browser API that allows you to run code when elements enter or leave a viewport or another element. Intersection Observer is natively supported, so it is a clean and performant solution to implement lazy loading or infinite scrolling.
Let’s take a look at an example where we can use Intersection Observer to lazy load images:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Lazy Load Images</title> <style> body { font-family: sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } img { width: 100%; max-width: 600px; margin: 30px auto; display: block; min-height: 500px; background-color: #ddd; object-fit: cover; transition: opacity 0.5s ease-in-out; opacity: 0; } img.loaded { opacity: 1; } </style> </head> <body> <img data-src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800" alt="Forest" /> <img data-src="https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?w=800" alt="Person" /> <img data-src="https://images.unsplash.com/photo-1516117172878-fd2c41f4a759?w=800" alt="Laptop" /> <img data-src="https://images.unsplash.com/photo-1741514376184-f7cd449a0978?w=800" alt="Mountains" /> <img data-src="https://images.unsplash.com/photo-1743062356649-eb59ce7b8140?w=800" alt="Desert" /> <img data-src="https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=800" alt="Bridge" /> <!-- Some more images here --> <img data-src="https://images.unsplash.com/photo-1518609878373-06d740f60d8b?w=800" alt="Sunset" /> <script> const images = document.querySelectorAll("img[data-src]"); const observer = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const img = entry.target; img.src = img.dataset.src; img.onload = () => img.classList.add("loaded"); img.removeAttribute("data-src"); observer.unobserve(img); }); }, { root: null, threshold: 0.1, } ); images.forEach((img) => observer.observe(img)); </script> </body> </html>
There are a few interesting things to note here. First, we have many img
tags with the image source URL set to the data-src
attribute. This prevents them from loading as soon as the page is rendered.
In the script
tag, we run some JavaScript code. First, we start by collecting all img
tags using a querySelector
. These will be used later for setting up the observer. Then, we create a single instance of IntersectionObserver
. To the constructor, we pass a function that gets called when the visibility of any of the observed elements changes.
Say, for example, when a user scrolls and the fourth image becomes visible in the viewport, the callback is triggered. The callback takes two arguments: entries
, which is a list of all observed elements whose visibility has changed, and observer
, which is the instance that can be used to perform cleanup actions.
Inside the callback, we only process entries (in our case, images) that intersect our viewport. For each image that is visible in the browser window, we add a src
attribute and remove the data-src
attribute. This then downloads the images and displays them in the window.
As a part of the cleanup, we unobserve the image using the unobserve
method on the observer. We pass two options to the constructor:
root
: This is the element the observer uses to check if the observed elements are intersecting or not. To set it to the viewport, we pass null
threshold
: This is a number between 0 and 1. This number decides after how much visibility the callback should be triggered. For example, if this value is set to 0.2, then the callback is triggered when the observing elements become 20% visible, either on entry or exitIntersection Observer is supported by all major browsers.
ResizeObserver
If you are building a web application with a complex UI (e.g., a drag-and-drop website builder or form builder), you might have faced the need to run code when a specific HTML element changes size. However, HTML elements don’t emit resize events (except for the window
object, which isn’t helpful in most cases). Previously, your best options were workarounds like MutationObserver
or setInterval
. But not anymore!
ResizeObserver
allows us to observe an HTML element and run code when its size changes. It is supported by all major browsers and can be used in a variety of use cases, including auto-scaling text, rendering responsive charts, handling virtualized lists, and building size-aware UI components. It’s also super easy to use.
Let’s see how we can use ResizeObserver
to auto-scale text based on its parent’s size:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Auto-Scaling Text</title> <style> body { font-family: sans-serif; padding: 2rem; } .container { width: 60%; height: 200px; border: 2px dashed #aaa; resize: horizontal; overflow: hidden; padding: 1rem; } .autoscale-text { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: font-size 0.2s ease; } </style> </head> <body> <div class="container" id="textBox"> <div class="autoscale-text" id="text"> Resize container to auto scale this text. </div> </div> <script> const container = document.getElementById("textBox"); const text = document.getElementById("text"); const resizeObserver = new ResizeObserver(() => { const containerWidth = container.clientWidth; const minFontSize = 12; const maxFontSize = 70; const minWidth = 200; const maxWidth = 800; const clampedWidth = Math.max( minWidth, Math.min(maxWidth, containerWidth) ); const ratio = (clampedWidth - minWidth) / (maxWidth - minWidth); const fontSize = minFontSize + (maxFontSize - minFontSize) * ratio; text.style.fontSize = fontSize + "px"; }); resizeObserver.observe(container); </script> </body> </html>
Let’s break this down. We have a div
with some text in it. This div
is resizeable (thanks to the resize:horizontal
CSS property). When this div changes its size, the font size of the text inside it auto-scales.
In the script
tag, we create a ResizeObserver
. To its constructor, we pass a callback function that is called when any of the observed elements’ sizes change. In the callback, we calculate the appropriate font size based on how large the div is with respect to its min and max width. We update the font size of the text element inline by updating its style attribute.
Similar to Intersection Observer, we can use one ResizeObserver
to observe multiple elements:
resizeObserver = new ResizeObserver((entries) => { // here entries will contain only those divs whose size has changed. entries.forEach(entry => { console.log(`Element resized:`, entry.target); console.log(`New size: ${entry.contentRect.width}x${entry.contentRect.height}`); }); }); multipleDivs.forEach((d) => resizeObserver.observe(d))
ResizeObserver
is supported by all major browsers.
The most common method for copying data from a web app to the system clipboard is to use the good old document.execCommand('copy')
. While this works, as web applications have become more complex and advanced, we’ve started to see the limitations of this approach. For starters, it only supports copying text to the clipboard — there is no support for pasting. It also doesn’t require users’ permission, and most importantly, it has been deprecated and is not recommended for new projects:
So, what’s the alternative? The brand new feature-rich Clipboard API!
The Clipboard API was introduced to provide programmatic access to the system clipboard. Because the clipboard is a system resource that is outside the control of the browser, the browser prompts the user, asking for permission to access the clipboard data. Also, it is mandatory to serve the webpage over HTTPS when using the Clipboard API in a non-local environment. The Clipboard API is fairly new but has been adopted by all major browsers and has been available in all browsers since June 2024.
Now, let’s see how we can use the Clipboard API to copy and paste text and images:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Clipboard API Example</title> </head> <body> <h2>đź“‹ Clipboard API Demo</h2> <button id="copyTextBtn">Copy Text to Clipboard</button> <div class="playground"> <div id="copy-from">This will be copied to the clipboard!</div> <div id="copy-to"><textarea></textarea></div> </div> <img id="copyImage" src="./sample1.png" alt="Sample" /> <button id="copyImageBtn">Copy Image to Clipboard</button> <script> // Copy Text document .getElementById("copyTextBtn") .addEventListener("click", async () => { try { const e = document.getElementById("copy-from"); await navigator.clipboard.writeText(e.innerText); alert("Text copied to clipboard!"); } catch (err) { console.error("Text copy failed:", err); alert("Failed to copy text."); } }); // Trigger copy event document.addEventListener("copy", () => alert("Something was copied to the system clipboard") ); // Trigger paste event document.addEventListener("paste", () => alert("Something was pasted from the system clipboard") ); // Copy Image document .getElementById("copyImageBtn") .addEventListener("click", async () => { const image = document.getElementById("copyImage"); try { const response = await fetch(image.src); const blob = await response.blob(); const clipboardItem = new ClipboardItem({ [blob.type]: blob }); await navigator.clipboard.write([clipboardItem]); alert("Image copied to clipboard!"); } catch (err) { console.error("Image copy failed:", err); alert("Failed to copy image."); } }); </script> </body> </html>
Let’s focus on the JavaScript written in the script
tag. First, we use the writeText
API on the Clipboard object to write contents to the system clipboard. This is similar to the execCommand('copy')
approach. We also add an event listener that runs when something is copied from the browser window. Remember, in the callback function, the data copied to the clipboard can’t be read; it can only be modified.
For copying images, we fetch the image from the link in the src
attribute of the image
tag, convert it to a blob, and then write it to the system clipboard. Additionally, we add another event listener that is triggered when something is pasted in the browser window.
In this article, we explored a few hidden gems in the JavaScript API:
structuredClone
: Allows deep cloning of JSON objectsAbortController
: Allows you to cancel already sent XHR requestsResizeObserver
: Allows developers to run code when an element changes its sizeThese native browser APIs unlock a wide range of use cases — from deep cloning complex objects and observing element size or visibility to handling clipboard operations and color picking — all without reaching for external packages. By understanding and leveraging these tools, you can build faster, more efficient, and cleaner web applications. And the best part? They’re all built right into the browser.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCheck out this guide, which ranks the top 10 JavaScript/HTML5 game engines by popularity, capability, and use case.
The React team officially released the first Release Candidate (RC). Let’s go over what’s new in RC and what it means for React developers.
The Model Context Protocol (MCP) provides a standardized way for AI models to access contextual information from diverse data sources.
Explore RBAC, ABAC, ACL, and PBAC access control methods and discover which one is ideal for your frontend use case.