Publishing Your First React Component to npm — the Complete Setup

Most tutorials stop at "it works on my machine." This one doesn't. Publishing to npm is where your library becomes real — installable, versioned, and usable by anyone. Here's the complete setup, including the fields most developers get wrong in package.json and how to inspect what you're actually shipping before it goes live.
There's a gap between "I built a component" and "I published a component." It's smaller than most developers think, but the details matter. A badly configured package.json means consumers get the wrong file, or no types, or a bundled copy of React that breaks their app. A missing .npmignore means your test files and Storybook stories go out with the package.
This article closes that gap. By the end, you'll have published v0.1.0 of your library and you'll understand every field that made it work.
The package.json fields that matter
Most developers treat package.json as boilerplate. For a library, it's configuration that directly affects how consumers import and use your code. Let's go field by field.
Here's what each field is doing:
name — use a scoped name (@your-username/ui-lib) if you want to namespace it under your npm username. Scoped packages can be published as public for free.
main — the CJS entry point. Used by older Node-based toolchains and bundlers that don't support ESM yet.
module — the ESM entry point. Used by modern bundlers like Vite and webpack 5. This is what most of your consumers will use.
types — points to the TypeScript declaration file. Without this, consumers get no autocomplete, no prop validation, no inline documentation.
exports — the modern way to define entry points. Takes precedence over main and module in Node 12+ and modern bundlers. Always define this alongside the legacy fields for maximum compatibility.
files — the whitelist of what gets included in the published package. Only dist goes out. Your src, node_modules, test files, and config files stay local.
sideEffects — tells bundlers which files have side effects and cannot be tree-shaken. Our component files are side-effect-free, but the built output files need to be listed here so styles are not accidentally eliminated.
peerDependencies — declares that React is required but not bundled. Consumers must have React installed themselves. This is what prevents duplicate React instances.
Setting up the .npmignore file
Even with the files field in package.json, an .npmignore file gives you an explicit blocklist as a second layer of protection:
Run npm pack --dry-run at any point to see exactly what would be included in the published package. Check this output before every release — it's the fastest way to catch an accidentally included file.
Setting up your npm account
If you don't have an npm account yet:
If you already have one, log in:
Verify you're logged in:
For a scoped package to be published publicly, you'll need to pass a flag when publishing. We'll cover that below.
Running a test publish with npm pack
Before anything goes live, inspect what you're actually shipping:
This creates a .tgz file locally — the exact archive that would be uploaded to npm. Unzip it and look inside:
You're looking for three things:
- The
dist/folder with ESM, CJS, and type declaration files - Nothing from
src/— no raw TypeScript source - No test files, no Storybook files, no config files
If anything unexpected is in there, fix the files field or .npmignore before publishing.
Publishing v0.1.0
Once the build looks right:
The --access public flag is required for scoped packages on the free npm plan. Without it, npm assumes you want to publish privately and will ask you to upgrade.
Go to https://www.npmjs.com/package/@your-username/ui-lib and verify the package is live. You should see the version, the description, and the weekly download count starting at zero.
Installing your own package to verify it works
The final check — install your package into a fresh Vite project and import a component:
Then in src/App.tsx:
If autocomplete works in your editor and the component renders, the publish is correct. If types are missing, the types field in package.json is pointing to the wrong file. If the import fails entirely, the exports field needs debugging.
What not to publish
A few things that trip up first-time library publishers:
Don't publish your src/ folder. Consumers should import from the built output, not your raw TypeScript. Shipping source creates a dependency on your internal structure.
Don't bundle React. We covered this in the Vite config — react and react-dom are external. If they end up in your bundle, you'll get "invalid hook call" errors in consumer apps.
Don't publish a CSS file without documenting it. If your library requires consumers to import a CSS file separately, that needs to be in your README. Otherwise consumers wonder why the styles aren't applying.
Up next: Versioning and changelogs for a UI library — how to do it without embarrassing yourself — semantic versioning in practice, what goes in a changelog, and how to cut a pre-release before you're ready to commit to v1.0.0.
Related: How to structure a UI library that scales — folders, exports, and naming conventions — the folder and export structure in the previous article directly shapes what your built output looks like. Worth reviewing if anything in the dist folder looked unexpected.
Muhammad Athar is the founder of DesignDev.io and the engineer behind everything you read here. He writes about the decisions behind building real products — the components, the architecture, and the tradeoffs that don't make it into the tutorial.
Photo by Compagnons on Unsplash

