Skip to main content
Frontend8 min readNovember 3, 2025

Creating a Component Library: From Scratch to Published Package

Build and publish a component library — architecture decisions, build tooling, documentation, versioning, and the lessons learned shipping real UI packages.

James Ross Jr.
James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Building a component library sounds straightforward until you start. You write some components, bundle them, publish to npm — done. But the gap between "a collection of components" and "a library teams actually want to use" is enormous. It is filled with decisions about API design, build configuration, tree-shaking, type exports, documentation, and versioning that do not have obvious right answers.

I have built internal component libraries for organizations and contributed to public ones. Here is what I wish someone had told me before I started.

Architecture and API Design

The most important decision is not which bundler to use — it is how your components expose their API. Every component has three surfaces: props, slots, and emitted events. The clarity and consistency of these surfaces determine whether your library is a joy or a frustration to use.

Establish conventions before writing a single component. Will boolean props use is prefixes (isDisabled vs disabled)? Will size props use string literals (sm | md | lg) or numeric scales? Will events use past tense (selected) or present tense (select)?

// Consistent prop patterns across components
interface ButtonProps {
 variant: 'primary' | 'secondary' | 'ghost'
 size: 'sm' | 'md' | 'lg'
 disabled?: boolean
 loading?: boolean
}

Interface InputProps {
 size: 'sm' | 'md' | 'lg' // Same scale as Button
 disabled?: boolean // Same name as Button
 error?: string
 modelValue: string
}

The patterns you set in your first five components become the patterns every future component follows. Getting them wrong means either living with inconsistency or doing a breaking API change later.

Compound components — where a parent and children work together — need special attention. A Tabs component with TabList, Tab, and TabPanel children requires shared state. In Vue, provide/inject handles this cleanly. The parent provides context, and children inject it. Do not rely on DOM structure assumptions or parent component name checks.

The Composition API makes it natural to extract the internal logic of each component into composables. This gives advanced users the ability to build custom UIs on top of your logic — a pattern that dramatically extends your library's useful life.

Build Tooling and Tree-Shaking

Your library must be tree-shakeable. If someone imports one button component, they should not get every component in the bundle. This requires specific build configuration.

Use named exports from a barrel file for the public API:

// src/index.ts
export { Button } from './components/Button'
export { Input } from './components/Input'
export { Select } from './components/Select'
export type { ButtonProps, InputProps, SelectProps } from './types'

Your build tool needs to produce ESM output with preserved module structure. Vite's library mode handles this, but you need to set build.rollupOptions.output.preserveModules to true. Without it, Rollup bundles everything into a single file and tree-shaking at the consumer level becomes impossible.

// vite.config.ts for library mode
export default defineConfig({
 build: {
 lib: {
 entry: resolve(__dirname, 'src/index.ts'),
 formats: ['es'],
 },
 rollupOptions: {
 external: ['vue'],
 output: {
 preserveModules: true,
 preserveModulesRoot: 'src',
 },
 },
 },
})

Mark framework dependencies as external. Vue should not be bundled with your library — the consuming application provides it. Same for any peer dependency like Tailwind CSS or a CSS-in-JS runtime.

Type declarations need to ship with the package. Use vue-tsc to generate .d.ts files that match your component signatures. Without types, your library is a black box for TypeScript users, which is most users in 2025. Ensure your package.json has the types field pointing to the declaration entry point.

Documentation as Product

A component library's documentation is its product. Developers evaluate libraries by reading docs, not source code. If they cannot figure out how to use a component in under a minute, they will use a different library.

The minimum for each component: a basic usage example, a prop table, and a visual rendering of every variant combination. Interactive playgrounds are better than static code blocks because developers can experiment without switching to their editor.

Storybook remains the standard tool for this, but alternatives like Histoire (built for Vue) offer tighter integration with Vue's single-file component format. Whichever tool you choose, the documentation must be deployed and publicly accessible. A README on npm is not enough.

Document the patterns your library recommends for common scenarios. How should users compose a form with your Input, Select, and Button components? What is the recommended way to handle form validation? These integration guides are often more valuable than individual component docs. The form validation patterns article covers approaches that work well when combined with a component library's form primitives.

Versioning and Breaking Changes

Semantic versioning is non-negotiable for a published library. But the hard part is defining what constitutes a breaking change. Obviously, removing a prop is breaking. But what about changing the default value of a prop? Adding a required prop? Changing the HTML structure of a rendered component in a way that could break CSS selectors?

My rule: if a consumer's code could fail to compile, render differently, or behave differently after updating without changing their code, it is a breaking change. This includes visual changes if your library is used for its visual output, which it almost certainly is.

Use a changelog that is written for humans, not generated from commit messages. "feat: add loading state to Button" is fine for a commit. For a changelog entry, write "Button now accepts a loading prop that shows a spinner and disables interaction. No changes needed for existing usage."

Deprecate before removing. When you need to rename a prop or change an API, add the new API alongside the old one with a console warning in development. Give consumers at least one minor version to migrate before the major version removes the deprecated API. This approach respects the time of every team that depends on your library, which is the difference between a library people trust and one they replace at the first opportunity.

Building a component library is a product development exercise disguised as a technical one. The code matters, but the developer experience around that code — types, docs, versioning discipline, migration guides — is what determines whether anyone uses it. For related thinking on how design system tokens feed into component libraries, that article covers the upstream decisions that shape your components.