The Rise of Zero-Dependency npm Libraries 2026
TL;DR
Zero-dependency packages are the most supply-chain-secure software you can install. No transitive vulnerabilities, no dependency conflicts, no lockfile bloat. In 2026, the best-designed libraries achieve full functionality with zero runtime dependencies — everything from state management (Zustand: 2KB, 0 deps) to ID generation (nanoid: 1KB, 0 deps) to event handling (mitt: 0.3KB, 0 deps). This is a deliberate design choice, not a constraint.
Key Takeaways
- Zero deps = zero transitive risk — the supply chain attack surface is you + your code
- Zustand, Jotai, nanoid, clsx, mitt — all zero runtime deps, all excellent
- "0 dependencies" in package.json — check
npm view package-name dependencies - Peer deps aren't the same — React as peer dep = you supply it, not the package
- The trend is accelerating: new packages launched in 2024-2026 default to zero deps
Why Zero Dependencies?
The value proposition of zero dependencies:
1. Supply chain security
→ Each dependency is a potential attack vector
→ A package with 5 deps can introduce 50+ transitive packages
→ Zero deps = only your direct install is in scope for supply chain attacks
→ leftpad incident (2016): 1 package = internet broke
→ event-stream (2018): malicious code added 5 levels deep in dep tree
2. Bundle size
→ Each dep adds to bundle size
→ Zero deps = exactly what you see on bundlephobia
→ No hidden 40KB for a utility you only use 1 function from
3. Conflict avoidance
→ Version conflicts happen when multiple packages depend on same lib
→ Zero deps eliminates this class of problem entirely
4. Predictability
→ No surprise breaking changes from dependencies you didn't update
→ Your package.json changes are your app's changes
5. Trust
→ You've audited the package; you trust it
→ With dependencies, you've audited one step; then need to trust the chain
The supply chain threat model for npm is different from the threat model for most software security. Traditional software security focuses on vulnerabilities in code you wrote — SQL injection, XSS, authentication flaws. Supply chain security focuses on code you didn't write and can't directly audit: the 200-400 transitive dependencies that appear in your node_modules when you install 20 direct packages.
The asymmetry is uncomfortable. When you install express, you're also installing bytes, debug, depd, encodeurl, escape-html, etag, finalhandler, fresh, ms, on-finished, parseurl, path-to-regexp, proxy-addr, qs, range-parser, safe-buffer, send, serve-static, setprototypeof, statuses, toidentifier, and vary. That's 22 packages for a single direct dependency. Each of those packages has its own maintainer, its own npm account that could be compromised, its own release process, and its own risk of a disgruntled or compromised maintainer pushing malicious code.
The security argument for zero-dependency packages isn't that transitive dependencies are inherently untrustworthy — most are fine. The argument is that the attack surface scales with the number of packages in your dependency tree, and zero-dependency packages contribute exactly one unit to that surface regardless of how complex their functionality. When evaluating Zustand vs Redux Toolkit for state management, the security-aware answer considers not just the API and bundle size, but that Zustand adds 1 package to your supply chain while RTK adds roughly 15.
Zero-Dependency Packages Worth Knowing
State Management
// Zustand — 2KB, 0 runtime dependencies
import { create } from 'zustand';
// React is a peer dependency (you provide it; Zustand doesn't ship it)
// The entire state management system in 2KB with no transitive deps
// Jotai — 3.1KB, 0 runtime dependencies
import { atom, useAtom } from 'jotai';
// Valtio — 2.5KB, 0 runtime dependencies
import { proxy, useSnapshot } from 'valtio';
// React is peer dep
// These replaced Redux which has:
// redux: 0 deps ✅ (surprisingly)
// react-redux: 2 deps
// @reduxjs/toolkit: 5+ deps (immer, redux-thunk, reselect, etc.)
// → RTK requires ~40KB transitive packages
Utilities
// nanoid — 1.1KB, 0 dependencies
import { nanoid } from 'nanoid';
const id = nanoid(); // 21-char URL-safe ID
// clsx — 0.5KB, 0 dependencies
import { clsx } from 'clsx';
const classes = clsx('foo', { bar: true, baz: false });
// mitt — 0.3KB, 0 dependencies
import mitt from 'mitt';
const emitter = mitt<{ event: string }>();
// ms — 0.5KB, 0 dependencies
import ms from 'ms';
ms('2 days') // 172800000
// klona — 1.1KB, 0 dependencies
import { klona } from 'klona';
const deep = klona(obj); // Deep clone, faster than JSON.parse/stringify
// bytes — 0.8KB, 0 dependencies
import bytes from 'bytes';
bytes(1024 * 1024) // '1MB'
HTTP
// In Node.js 18+: native fetch — 0 dependencies
const data = await fetch('https://api.example.com').then(r => r.json());
// No npm install needed
// undici — 0 dependencies (this is the actual Node.js HTTP impl)
import { request } from 'undici';
// Highest performance, built into Node.js core
Validation
// Valibot — designed to be tree-shakeable, 0 runtime deps
import { object, string, parse } from 'valibot';
// Each imported function is independent — zero unused code
// ArkType — 0 runtime dependencies
import { type } from 'arktype';
const user = type({ name: 'string', age: 'number' });
The packages listed above represent a specific tier of zero-dependency success: achieving non-trivial functionality with zero runtime dependencies while maintaining high download velocity. What they share is not just the dependency count but a design philosophy — each solves a focused problem without accumulating scope that would require external libraries.
Nanoid is illustrative: generating cryptographically random URL-safe IDs sounds like it might require a crypto library. It doesn't — the Web Crypto API (crypto.getRandomValues()) is built into browsers and Node.js. Nanoid is 22 lines of core implementation code that calls a built-in API. The npm package exists primarily to provide the import path and TypeScript types; the actual implementation would fit in a tweet.
Clsx is similar: conditional CSS class name concatenation sounds trivial, and it is. The package is ~150 bytes unminified. Its value isn't algorithmic complexity — it's the API design (accepting strings, objects, and arrays in any combination) and the established import path that tools like Tailwind's VS Code extension can recognize. A zero-dependency package can provide enormous value simply by being the canonical, well-typed implementation of a common pattern.
This explains why zero-dependency packages at the utility end of the spectrum have almost universally won against their dependency-heavy predecessors. clsx over classnames (which had dependencies). mitt over EventEmitter3 (which is itself zero-dep, but larger). nanoid over uuid (which has dependencies and is 5x larger). The pattern: when the problem is small and well-defined, the right implementation is small and dependency-free.
The Peer Dependency Distinction
# Zero dependencies ≠ no peer dependencies
# These are different:
# Dependencies (ships WITH the package, adds to your node_modules):
# npm view lodash dependencies → {} (zero deps — good)
# npm view react-router-dom dependencies → { react, @remix-run/... } (ships deps)
# Peer dependencies (YOU provide these, NOT shipped by the package):
# npm view zustand peerDependencies → { react: ">= 16.8" }
# This means: "zustand REQUIRES react but expects YOU to install it"
# Zustand itself has zero runtime dependencies
# How to check:
npm view package-name dependencies
# Empty object → zero runtime deps
npm view package-name peerDependencies
# Lists what you must provide but package doesn't ship
# The ideal pattern:
# peerDependencies: { react: "*" } ← you control the react version
# dependencies: {} ← zero hidden packages
# devDependencies: { react, typescript, ... } ← just for their tests
The peer dependency distinction matters practically in two ways. First, it's how zero-dependency packages with framework integrations actually work. Zustand needs React — but it declares React as a peer dependency, meaning React appears in your package.json (where you control the version) rather than inside Zustand's node_modules. The result: no React version conflicts, no duplicate React installations in your bundle, no npm deduplication surprises. The package works with whatever React version you're already using, including React 18 and React 19.
Second, peer dependencies can cause confusion during installation. npm v7+ auto-installs peer dependencies, which means a "zero dependency" package might show more than one install if you don't already have its peer deps. This doesn't contradict the zero-dependency claim — peer deps aren't shipped by the package — but it can look confusing in a dry-run install. The correct way to verify zero runtime dependencies is npm view package-name dependencies, not counting install lines.
For libraries that declare framework peer dependencies (React, Vue, Angular), a zero-dependency claim is strongest when the package uses the framework's public API rather than its internals. Zustand uses useSyncExternalStore, a stable React 18 hook, rather than React's internal fiber structure. This means Zustand is forward-compatible with any future React version that maintains this hook's contract — a much stronger guarantee than packages that tap into React internals.
How Well-Designed Libraries Achieve Zero Deps
// Technique 1: Use Web Platform APIs instead of utility packages
// Instead of: npm install uuid
const id = crypto.randomUUID(); // Web Crypto API, built into Node 18+
// Instead of: npm install node-fetch
const data = await fetch(url).then(r => r.json()); // Built-in Node 18+
// Instead of: npm install deep-equal
const equal = JSON.stringify(a) === JSON.stringify(b); // For JSON-safe objects
// Or: structuredClone comparison (Node 17+)
// Technique 2: Tiny implementation instead of heavy library
// Zustand's entire core (~200 lines of TypeScript):
// - Uses React.useSyncExternalStore (built-in React 18)
// - Subscription model using a Set (built-in JS)
// - No immer, no redux patterns, no middleware by default
// Result: complete state management in 200 lines, 0 deps
// Technique 3: Bundling micro-dependencies they control
// dayjs bundles all locales as optional plugin files
// No external dependency on a locale data package
// You get what you import, nothing extra
// Technique 4: Being opinionated about scope
// A good library does one thing and doesn't need 10 utilities to do it
// Scope creep requires dependencies; focused scope doesn't
The four techniques for achieving zero dependencies — using Web Platform APIs, implementing tiny alternatives, bundling controlled micro-dependencies, and being opinionated about scope — all share a common prerequisite: deliberate constraint. Maintaining zero dependencies requires actively rejecting the path of least resistance (adding a utility library) in favor of implementing the functionality yourself using platform primitives.
This constraint has become easier to maintain as the Web Platform has matured. In 2018, if you needed a cryptographically random ID, you had two options: use uuid (which had its own dependencies) or write your own implementation against the platform's crypto API (which required knowing the platform API was stable and available). In 2026, crypto.randomUUID() is available in every modern environment with no research required. structuredClone() replaced deep-clone utilities. Object.groupBy() (ES2024) is absorbing Lodash's _.groupBy(). Array.at(-1) replaced the _.last() pattern.
Each platform API addition makes a previously dependency-requiring function implementable in a single line. The cumulative effect: a developer building a utility library in 2026 can cover more ground without external dependencies than was possible in 2019. The Web Platform roadmap suggests this trend continues — proposals for Array.zip(), Uint8Array.fromBase64(), and various Temporal date operations are in progress. Every one of these will make a category of npm utilities unnecessary.
The Math: How Dependencies Multiply
# Check how many packages a single dependency brings:
npm install --dry-run express 2>&1 | tail -3
# added 57 packages from 44 contributors
npm install --dry-run fastify 2>&1 | tail -3
# added 8 packages
npm install --dry-run zustand 2>&1 | tail -3
# added 1 package ← just zustand itself
# The difference:
# express: 57 packages to audit, 57 potential attack vectors
# fastify: 8 packages
# zustand: 1 package
# At project scale, this compounds:
# A project with 20 direct deps might have 200-500 transitive deps
# Every one is in your supply chain
# Zero-dep packages: the "1 package" count is real
# They don't multiply
The install count difference has a practical consequence beyond security: npm audit. Every transitive dependency is scanned by npm audit, and every vulnerability discovered in any of them generates an advisory. A project with 500 transitive packages will regularly encounter npm audit warnings about packages 4-5 levels deep in the dependency tree that the team has no ability to update directly. Many of these vulnerabilities are in code paths that don't execute in the project's context, but npm audit can't distinguish that — it reports all advisories regardless of exploitability.
Teams with large transitive dependency trees spend meaningful engineering time triaging npm audit output, updating packages to force transitive dependency version bumps, and documenting why specific advisory-flagged packages can't be updated. Zero-dependency packages don't contribute to this overhead. A project that systematically prefers zero-dependency utilities reduces the npm audit noise proportionally, making the remaining advisories (in genuinely complex packages) easier to focus on and address.
For teams operating in enterprise environments with formal security review requirements — SBOM generation, license scanning, ongoing vulnerability management — reducing transitive dependency counts has a measurable, direct impact on compliance workload. The zero-dependency preference is increasingly codified in security-aware teams' contribution guidelines, not just as a performance consideration but as a security engineering practice.
Zero-Dep Alternatives for Common Tasks
| Task | With Dependencies | Zero-Dep Alternative |
|---|---|---|
| Unique IDs | uuid (14KB, 1 dep) | crypto.randomUUID() built-in |
| Deep clone | lodash.clonedeep (5KB) | structuredClone() built-in |
| Directory creation | mkdirp (0.5KB, 1 dep) | fs.mkdirSync(path, {recursive:true}) |
| Recursive delete | rimraf (1KB, 1 dep) | fs.rmSync(path, {recursive:true, force:true}) |
| HTTP fetch | node-fetch (0.5KB) | fetch() built-in Node 18+ |
| Event emitter | eventemitter3 (2KB, 0 deps) | mitt (0.3KB, 0 deps) |
| String colors | chalk (1.5KB, varies) | picocolors (0.3KB, 0 deps) |
| State management | Redux (10KB+) | Zustand (2KB, 0 deps) |
| Date formatting | moment (72KB) | dayjs (2.7KB, 0 deps) |
The alternatives table above reveals a pattern: the zero-dependency alternatives aren't just smaller — they're also conceptually simpler. structuredClone() doesn't require learning an API; it's a function call. fs.mkdirSync(path, { recursive: true }) is self-documenting in a way that mkdirp isn't. The built-in fetch() requires no configuration for most use cases. This simplicity is a secondary benefit of zero-dependency packages: fewer abstractions means fewer mental models.
The table also illustrates where native APIs haven't fully arrived. chalk vs picocolors is still a real choice — both are npm packages because terminal color handling via ANSI escape codes isn't exposed through a standard Web Platform API. For terminal output, picocolors (0.3KB, 0 deps) is the zero-dependency choice, but you still need an npm package. Similarly, there's no built-in state management primitive — Zustand is the zero-dep choice, not a built-in. The categories that still require npm packages are the ones where the problem is inherently application-level rather than platform-level.
One category not well represented in the table: testing utilities. vitest has dependencies (it depends on vite for its transformation pipeline). jest has many dependencies. There isn't a mainstream zero-dependency test runner for TypeScript projects — the built-in node:test module is close, but lacks TypeScript support without additional tooling. This is a gap in the zero-dependency landscape that may be filled as the platform's test runner matures.
The Zero-Dep Checklist
# Before installing any package, verify:
# 1. Check runtime deps:
npm view package-name dependencies
# Empty? → Zero deps ✅
# 2. Verify it's not cheating (bundling huge libs):
npx bundlephobia-cli package-name
# If zero deps but still 50KB+: it bundled something
# 3. Check peer deps (fine — you control these):
npm view package-name peerDependencies
# 4. Ask: is there a built-in alternative?
# Node.js 18-22 added: fetch, crypto.randomUUID, structuredClone,
# fs.rm, fs.mkdir recursive, Web Streams API, AbortController
# Modern browsers: same APIs
# Check MDN/Node.js docs before npm installing anything
# 5. For utilities you do need: prefer the zero-dep option
# Same functionality, less risk, smaller bundle
When a Package With Dependencies Is the Right Choice
Zero dependencies is a preference, not an absolute rule. Some packages with dependencies are the clearly right choice for their use cases, and the dependency count shouldn't override that judgment.
The clearest example: vite. Vite has a significant dependency tree (esbuild, rollup, and others), but the value it provides — near-instant dev server startup, plugin ecosystem, optimized production builds — is inseparable from those dependencies. No reasonable developer would choose a zero-dependency bundler over Vite for its supply chain profile.
The decision framework: does the package's dependency count add risk that isn't justified by the value it provides? For Vite, clearly not. For a utility that joins array elements with commas, adding 3 transitive dependencies for that functionality is harder to justify.
The key questions: How critical is the functionality to your application? (A dependency in your core build pipeline vs a formatting utility have different risk profiles.) Are the transitive dependencies themselves reputable and actively maintained? (esbuild is well-maintained by a single author; the risk is known.) Could you implement the functionality yourself in under 30 minutes without the package? (If yes, the zero-dep alternative is often worth pursuing.)
The practical heuristic that experienced JavaScript developers use: prefer zero-dependency packages for utilities, formatters, and single-purpose tools. Accept dependencies for frameworks, build tools, and packages where the functionality genuinely requires external capabilities. Draw the line based on how critical the functionality is and how auditable the dependency tree remains.
Supply Chain Attack Reality in 2026
The supply chain attacks that prompted the zero-dependency movement weren't theoretical — they've been regular, real, and costly.
The major incidents that shaped the current security culture in npm: The event-stream compromise (2018) — a malicious contributor added code to steal Bitcoin wallets to a package 5 levels deep in a common dependency tree. 8 million weekly downloads affected. This incident proved that the transitive dependency risk was not theoretical; adversaries actively look for high-impact packages deep in popular dependency chains.
The ua-parser-js takeover (2021) — account takeover of a widely-used package, malicious code pushed that installed a cryptominer and credential stealer. 4+ major companies reported compromised environments.
The node-ipc supply chain protest (2022) — a maintainer added code that deleted files on computers in Russia and Belarus as a protest against the Ukraine invasion. The code executed on every npm install for affected packages. Beyond intent (protest, not malicious in the traditional sense), it demonstrated that any package can be weaponized by its maintainer at any time.
The zero-dependency response: these incidents accelerated demand for packages with minimal dependency footprints. Teams started auditing their package.json more carefully, checking npm audit, and preferring alternatives that reduced transitive exposure. The SBOM (Software Bill of Materials) requirement in US federal contracts (from EO 14028, 2021) further pushed enterprise users toward dependency-minimal software.
In 2026, zero-dependency packages are increasingly preferred in enterprise and regulated environments not just for performance, but for security compliance.
The Author's Perspective: Why Maintainers Choose Zero Deps
Interviews with maintainers of successful zero-dependency packages reveal consistent motivations. For the creators of clsx, mitt, and similar utility packages, the choice was pragmatic: the functionality simply doesn't require external dependencies. A utility that classifies CSS class names doesn't need lodash. A simple event emitter doesn't need EventEmitter3. Scope discipline — doing one thing without feature creep — naturally produces zero dependencies.
For larger packages like Zustand and Valtio, the zero-dependency goal required active refusal to use available libraries. Zustand's core could have imported immer for immutable updates (Redux Toolkit does this). Instead, the maintainers built a lighter mechanism using React's own useSyncExternalStore. The reward: Zustand's entire install footprint is exactly one package.
The ecosystem effect: when popular packages demonstrate that zero dependencies is achievable at non-trivial complexity levels, it raises the bar for new packages. Developers who've seen Zustand achieve complete state management in 2KB with zero deps are less forgiving of packages that bundle 8 utilities for simple functionality.
The trend in new package launches: packages launched in 2024-2026 are significantly more likely to have zero or minimal dependencies than packages from 2018-2022, both because of the security awareness and because the Web Platform APIs have eliminated many previously necessary utility dependencies.
Trending Zero-Dep Packages to Watch
Beyond the established zero-dep packages (nanoid, clsx, mitt, Zustand), several newer zero-dependency packages have emerged that are worth tracking for 2026-2027 projects:
Zag.js (for UI state machines, 0 deps) — the headless component library behind Park UI, providing accessible state machine primitives without coupling to any framework.
Hono (0 deps, Web Standards) — the HTTP framework for edge computing has zero runtime dependencies and runs in Deno, Bun, Cloudflare Workers, and Node.js. The zero-dep constraint forces it to use Web Standards APIs exclusively, which makes the code maximally portable.
tinybase (0 deps) — reactive data stores for local-first applications.
Effect — technically has dependencies but is a self-contained ecosystem; its zero external dependency model for the core modules is worth tracking.
The indicator to watch: packages that score zero on npm view package-name dependencies AND ship TypeScript types natively AND have growing download velocity are the ones worth evaluating seriously. They represent the convergence of security awareness, bundle optimization, and modern TypeScript-first development in a single choice.
Compare bundle sizes and dependency counts for npm packages at PkgPulse.
See also: Bun vs Vite and AVA vs Jest, npm Dependency Trees: Most Nested Packages 2026.
See the live comparison
View date fns vs. dayjs on PkgPulse →