Your browser already has more built-in features than you think.
Let's be honest: how many npm packages have you installed without checking whether JavaScript or the browser could already do the same thing?
Need to detect when an element enters the viewport? Install a library.
Need to copy text? Install a package.
Need to clone an object? Install Lodash.
Need to make HTTP requests? Install Axios.
Need to format dates? Install Moment.js.
But here's the thing:
Modern browsers have evolved. And some of the dependencies we used to consider essential are no longer necessary for everyday tasks.
The Web Platform provides powerful native APIs that can help you reduce dependencies, simplify your code, improve performance, and keep your bundle smaller.
In this article, we'll explore 15 things you can do without installing another npm package.
Let's get into it.
1. Stop Using jQuery for DOM Selection
Instead of: jquery
Remember when almost every JavaScript project started with jQuery?
Today, modern browsers provide native DOM methods that handle most everyday selection and manipulation tasks.
Before:
const element = $("#app"); element.addClass("active"); element.text("Hello World"); After:
const element = document.querySelector("#app"); element.classList.add("active"); element.textContent = "Hello World"; Need to select multiple elements?
const buttons = document.querySelectorAll("button"); buttons.forEach((button) => { button.addEventListener("click", () => { console.log("Clicked!"); }); }); Need to find the closest parent matching a selector?
const card = event.target.closest(".card"); Why use the native API?
- No additional dependency.
- Direct access to browser DOM functionality.
- Less abstraction for simple operations.
Keep jQuery if: You're maintaining a legacy application or relying heavily on its ecosystem and plugins.
2. Stop Using Axios for Simple HTTP Requests
Instead of: axios
The Fetch API is built directly into modern browsers.
You don't need an external library just to make an HTTP request.
Before:
import axios from "axios"; const response = await axios.get("/api/users"); console.log(response.data); After:
const response = await fetch("/api/users"); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const users = await response.json(); console.log(users); Posting JSON is straightforward too:
const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: "Hamed", role: "Developer", }), }); const result = await response.json(); One important difference: Fetch doesn't reject its Promise for HTTP errors such as 404 or 500. You must check response.ok.
Keep Axios if: You need interceptors, automatic request/response transformations, extensive client configuration, or an existing shared HTTP infrastructure.
3. Stop Using Lodash for Deep Cloning
Instead of: _.cloneDeep()
JavaScript now has a native structuredClone() function.
It handles many types that JSON-based cloning cannot correctly preserve.
Before:
import cloneDeep from "lodash/cloneDeep"; const copy = cloneDeep(originalObject); After:
const copy = structuredClone(originalObject); For example:
const original = { name: "Hamed", createdAt: new Date(), tags: new Set(["JavaScript", "React"]), scores: new Map([ ["JavaScript", 95], ["React", 90], ]), }; const copy = structuredClone(original); console.log(copy.createdAt instanceof Date); // true console.log(copy.tags instanceof Set); // true console.log(copy.scores instanceof Map); // true It also supports circular references.
Important: structuredClone() cannot clone functions or DOM nodes. It also doesn't preserve custom class prototypes and methods.
Keep Lodash if: You need specialized cloning behavior or support for values that structured cloning cannot handle.
4. Stop Using Scroll Event Libraries for Visibility Detection
Instead of: lazysizes, custom scroll listeners, or visibility plugins.
Want to know when an element enters the viewport?
Meet IntersectionObserver.
It allows you to observe elements without manually checking their positions on every scroll event.
const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log("Element is visible!"); entry.target.classList.add("visible"); observer.unobserve(entry.target); } }); }); document.querySelectorAll(".animate-on-scroll").forEach((element) => { observer.observe(element); }); Useful for:
- Lazy loading.
- Infinite scrolling.
- Scroll-triggered animations.
- Analytics visibility tracking.
For ordinary image lazy loading, you might not even need JavaScript:
<img src="image.jpg" loading="lazy" alt="Example" /> Keep a library if: You need complex scroll choreography, advanced animation timelines, or sophisticated visibility management.
5. Stop Using Resize Event Hacks for Element Sizing
Instead of: Custom resize listeners and element-resize plugins.
The ResizeObserver API lets you monitor changes to an individual element's dimensions.
Traditional approach:
window.addEventListener("resize", () => { // Check element dimensions manually }); Native approach:
const observer = new ResizeObserver((entries) => { entries.forEach((entry) => { console.log("Width:", entry.contentRect.width); console.log("Height:", entry.contentRect.height); }); }); const container = document.querySelector(".container"); observer.observe(container); Why is this better?
Because an element can change size without the browser window changing size.
For example:
- A sidebar opens.
- A parent container changes dimensions.
- A grid layout changes.
- Content is dynamically inserted.
ResizeObserver handles these cases.
Keep a library if: You need higher-level responsive layout behavior or complex element measurement utilities.
6. Stop Using Moment.js for Basic Date Formatting
Instead of: moment() or basic date-formatting packages.
JavaScript already provides the Intl API.
Need to format dates based on a user's locale?
const date = new Date("2026-11-15T12:30:00"); const formatted = new Intl.DateTimeFormat("en-US", { dateStyle: "full", timeStyle: "short", }).format(date); console.log(formatted); Want a different locale?
console.log( new Intl.DateTimeFormat("de-DE").format(date) ); console.log( new Intl.DateTimeFormat("fa-IR").format(date) ); console.log( new Intl.DateTimeFormat("ja-JP").format(date) ); You can also format relative time:
const relativeTime = new Intl.RelativeTimeFormat("en", { numeric: "auto", }); console.log(relativeTime.format(-2, "day")); // 2 days ago And currency:
const price = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }); console.log(price.format(1299.99)); // $1,299.99 Keep a date library if: You need advanced date arithmetic, parsing, recurring schedules, or complex timezone operations. Intl formats dates; it isn't a complete date manipulation library.
7. Stop Installing UUID Packages for Basic IDs
Instead of: uuid
If you only need a random UUID v4, the browser already provides one.
const id = crypto.randomUUID(); console.log(id); Example output:
f47ac10b-58cc-4372-a567-0e02b2c3d479 You can use it for:
- Temporary identifiers.
- Client-side records.
- UI keys.
- Local application data.
Important: crypto.randomUUID() requires a secure context in browsers (usually HTTPS or localhost).
Keep the UUID package if: You need other UUID versions, custom generation, or support for environments without the native API.
8. Stop Using Libraries for Query String Manipulation
Instead of: qs or manually splitting URL strings.
JavaScript provides URL and URLSearchParams.
Need to read query parameters?
const params = new URLSearchParams(window.location.search); const search = params.get("search"); const page = params.get("page"); console.log(search); console.log(page); Need to create a URL?
const url = new URL("https://example.com/products"); url.searchParams.set("category", "laptops"); url.searchParams.set("page", "2"); console.log(url.toString()); Need to remove a parameter?
url.searchParams.delete("page"); Need to check whether a parameter exists?
if (url.searchParams.has("category")) { console.log("Category filter exists"); } Keep a library if: You need complex nested query structures or serialization rules that URLSearchParams doesn't provide.
9. Stop Using Clipboard Libraries for Copy Buttons
Instead of: clipboard.js
Copying text is now a built-in browser capability.
async function copyText(text) { try { await navigator.clipboard.writeText(text); console.log("Copied successfully!"); } catch (error) { console.error("Clipboard failed:", error); } } copyText("Hello from JavaScript!"); A practical React example:
function CopyButton({ text }) { const handleCopy = async () => { try { await navigator.clipboard.writeText(text); alert("Copied!"); } catch { alert("Unable to copy text"); } }; return ( <button onClick={handleCopy}> Copy </button> ); } Important: Clipboard access generally requires HTTPS or localhost and may require user interaction or permission.
Keep a library if: You need extensive legacy-browser fallbacks or advanced clipboard formats.
10. Stop Using Modal Libraries for Simple Dialogs
Instead of: Basic modal implementations with custom JavaScript.
HTML has a native <dialog> element.
<button id="openModal">Open Modal</button> <dialog id="myDialog"> <h2>Hello!</h2> <p>This is a native HTML dialog.</p> <form method="dialog"> <button>Close</button> </form> </dialog> JavaScript:
const dialog = document.querySelector("#myDialog"); document.querySelector("#openModal").addEventListener("click", () => { dialog.showModal(); }); You automatically get:
- Native modal behavior.
- Escape-key dismissal.
- Focus management.
- A top-layer rendering mechanism.
- A customizable backdrop.
You can style the backdrop too:
dialog::backdrop { background: rgb(0 0 0 / 60%); backdrop-filter: blur(4px); } Keep a library if: You need advanced animations, complex nested dialogs, or highly customized interaction patterns.
11. Stop Using Libraries for Simple Tooltips and Popovers
Instead of: Basic dropdown and popover implementations.
The native Popover API makes simple interactive UI elements much easier.
<button popovertarget="myPopover"> Open Menu </button> <div id="myPopover" popover> <p>Hello from the Popover API!</p> </div> That's it.
No JavaScript event listeners required.
You can also control it programmatically:
const popover = document.querySelector("#myPopover"); popover.showPopover(); popover.hidePopover(); popover.togglePopover(); Useful for:
- Simple menus.
- Share panels.
- Contextual content.
- Lightweight popovers.
Important: The Popover API handles showing and dismissing elements, not all positioning or tooltip behavior. For sophisticated positioning, Floating UI is still valuable.
12. Stop Using Custom Event Listeners for DOM Changes
Instead of: Polling the DOM or repeatedly checking whether an element exists.
Meet MutationObserver.
It lets you observe changes to the DOM tree.
const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { console.log("DOM changed:", mutation.type); }); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, }); You can also watch a specific element:
const target = document.querySelector("#app"); const observer = new MutationObserver(() => { console.log("Something changed inside #app"); }); observer.observe(target, { childList: true, subtree: true, }); Useful for:
- Browser extensions.
- Third-party DOM integrations.
- Monitoring dynamically inserted content.
- Integrating with legacy interfaces.
Keep a library if: You need a higher-level abstraction for complex DOM synchronization.
13. Stop Using Libraries for Basic Animations
Instead of: Installing an animation library for simple transitions.
CSS already handles many UI animations.
.card { opacity: 0; transform: translateY(20px); transition: opacity 300ms ease, transform 300ms ease; } .card.visible { opacity: 1; transform: translateY(0); } For animations controlled directly through JavaScript, use the Web Animations API:
const element = document.querySelector(".card"); element.animate( [ { opacity: 0, transform: "translateY(20px)", }, { opacity: 1, transform: "translateY(0)", }, ], { duration: 400, easing: "ease-out", fill: "forwards", } ); You can also control the animation:
const animation = element.animate( [{ transform: "scale(1)" }, { transform: "scale(1.1)" }], { duration: 500, fill: "forwards", } ); animation.pause(); animation.play(); animation.reverse(); Keep a library if: You need complex physics-based animations, gesture-driven interactions, orchestration, or sophisticated layout transitions.
14. Stop Writing Custom Request Cancellation Logic
Instead of: Extra utilities just to cancel outdated requests.
The browser provides AbortController.
This is especially useful when users type into a search box and each keystroke triggers a new request.
let controller; async function searchUsers(query) { controller?.abort(); controller = Source:https://dev.to/hamedfarazi/stop-using-javascript-libraries-for-these-15-things-3ppi