Creating and Publishing a Svelte Component Package
Building reusable UI components is one of the most valuable things you can do for your team or the open-source community. Svelte makes this particularly clean — components compile down to vanilla JavaScript with zero runtime overhead.
There are two main approaches:
- Standalone package: A single npm package containing your component library
- Component library monorepo: Multiple packages in one repo (e.g., with Turborepo or pnpm workspaces)
For most cases, the standalone approach is the right starting point. Here’s how to do it.
Project Setup
Use the official sv CLI to scaffold a Svelte library project:
npx sv create my-svelte-components
During the questionnaire:
- Select “SvelteKit library” as the project type
- Choose TypeScript (strongly recommended for consumer DX)
- Optionally add Prettier, ESLint, TailwindCSS
- Choose your preferred package manager (
bun,pnpm,npm)
This creates a project with the correct structure for an npm-publishable library.
Project Structure
my-svelte-components/
├── src/
│ ├── lib/
│ │ ├── components/
│ │ │ ├── Button.svelte
│ │ │ ├── Modal.svelte
│ │ │ └── index.ts ← re-export components here
│ │ └── index.ts ← main library entry point
│ └── routes/ ← demo/docs app (not published)
│ └── +page.svelte
├── package.json
├── svelte.config.js
└── tsconfig.json
Writing Your Components
<!-- src/lib/components/Button.svelte -->
<script lang="ts">
type Variant = 'primary' | 'secondary' | 'ghost';
type Size = 'sm' | 'md' | 'lg';
interface Props {
variant?: Variant;
size?: Size;
disabled?: boolean;
onclick?: () => void;
children?: import('svelte').Snippet;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
onclick,
children,
}: Props = $props();
</script>
<button
class="btn btn-{variant} btn-{size}"
{disabled}
{onclick}
>
{@render children?.()}
</button>
<style>
.btn {
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: none;
transition: background-color 150ms ease;
}
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-secondary { background: #e5e7eb; color: #111; }
.btn-ghost { background: transparent; border: 1px solid currentColor; }
.btn-sm { padding: 4px 12px; font-size: 0.875rem; }
.btn-md { padding: 8px 16px; font-size: 1rem; }
.btn-lg { padding: 12px 24px; font-size: 1.125rem; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
</style>
Setting Up Exports
Export everything from src/lib/index.ts:
// src/lib/index.ts
export { default as Button } from "./components/Button.svelte";
export { default as Modal } from "./components/Modal.svelte";
export { default as Card } from "./components/Card.svelte";
// Also export any TypeScript types
export type { ButtonProps } from "./components/Button.svelte";
Configuring package.json
The sv create tool generates most of this, but verify these key fields:
{
"name": "@your-scope/my-svelte-components",
"version": "0.1.0",
"description": "A reusable Svelte component library",
"type": "module",
"main": "./dist/index.js",
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "vite build && tsc --emitDeclarationOnly",
"dev": "vite dev",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/package": "^2.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
}
}
The "svelte" field tells bundlers that understand Svelte to use the Svelte-specific build. The "exports" map handles modern Node.js module resolution.
Building the Library
# Build the library (outputs to /dist)
bun run build
# Type-check everything
bun run check
The build will output compiled .js files and .d.ts type declarations to /dist.
Local Development and Testing
Test your components in the local demo app (src/routes/+page.svelte) before publishing:
<!-- src/routes/+page.svelte -->
<script>
import { Button, Card } from '$lib';
</script>
<Button variant="primary" onclick={() => alert('clicked!')}>
Click Me
</Button>
<Button variant="secondary" size="lg">
Large Secondary
</Button>
Run the dev server:
bun dev
Publishing to npm
# Log in to npm
npm login
# Build the library
bun run build
# Publish (scoped packages are private by default — use --access public for open source)
npm publish --access public
For a first release, version 0.1.0 is conventional. Use semantic versioning:
- Patch (
0.1.1): Bug fixes, no API changes - Minor (
0.2.0): New features, backward compatible - Major (
1.0.0): Breaking changes
Using Your Published Library
Once published, consumers install and use it like any other package:
npm install @your-scope/my-svelte-components
<script>
import { Button, Card } from '@your-scope/my-svelte-components';
</script>
<Button variant="primary">Hello from the library!</Button>
Because it exports proper Svelte components (not pre-compiled), consumers get full Svelte optimization, tree-shaking, and TypeScript autocomplete.
Tips for a Good Component Library
- Document with JSDoc — TypeScript consumers see docs in their IDE
- Provide sensible defaults — every prop should have a reasonable default
- Support slots/snippets — composable components are more useful
- Export TypeScript types — makes consuming the library much better
- Write a changelog — consumers need to know what changed between versions
- Set up a GitHub Actions CI — run
svelte-checkon every PR
💬 Want to learn, build, and grow with a community of developers? Join the King Technologies Discord — where code meets community!