diff --git a/.gitignore b/.gitignore
index 4a90eccd..2c9ee046 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ yarn.lock
# Build outputs
dist/
build/
+.svelte-kit/
*.tsbuildinfo
# Logs
diff --git a/README.md b/README.md
index 9d958cc5..7e0e28b3 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ SVG sources live in [`assets/`](./assets), organized by category — each framew
| | Package | Version | Links |
| ------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
| `@appica/icons-react` | [](https://www.npmjs.com/package/@appica/icons-react) | [Docs](https://appica.dev/ui/icons) · [Source](packages/react) |
+| **Svelte 5** | `@appica/icons-svelte` | [](https://www.npmjs.com/package/@appica/icons-svelte) | [Docs](https://appica.dev/ui/icons) · [Source](packages/svelte) |
More variants are planned — webfont, Vue and more.
diff --git a/package.json b/package.json
index bc276e77..ae98769e 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,9 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"pack:react": "pnpm --filter @appica/icons-react build && pnpm --filter @appica/icons-react exec pnpm pack",
- "release": "pnpm --filter @appica/icons-react publish"
+ "pack:svelte": "pnpm --filter @appica/icons-svelte build && pnpm --filter @appica/icons-svelte exec pnpm pack",
+ "release": "pnpm --filter @appica/icons-react publish",
+ "release:svelte": "pnpm --filter @appica/icons-svelte publish"
},
"devDependencies": {
"prettier": "^3.9.4"
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
new file mode 100644
index 00000000..cb49f173
--- /dev/null
+++ b/packages/svelte/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@appica/icons-svelte",
+ "version": "1.0.0",
+ "description": "Tree-shakeable SVG icon components for Svelte 5 — use standalone or as part of the Appica UI.",
+ "license": "MIT",
+ "homepage": "https://appica.dev/ui/icons",
+ "author": {
+ "name": "Appica",
+ "url": "https://appica.dev"
+ },
+ "keywords": [
+ "icons",
+ "icon",
+ "svg",
+ "svelte",
+ "svelte5",
+ "typescript",
+ "ui-icons",
+ "appica",
+ "ui-components"
+ ],
+ "type": "module",
+ "svelte": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "sideEffects": [
+ "**/*.css"
+ ],
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "bugs": {
+ "url": "https://github.com/appica-dev/appica-icons/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/appica-dev/appica-icons.git",
+ "directory": "packages/svelte"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "svelte": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "clean": "rm -rf dist src/icons",
+ "codegen": "tsx scripts/build.ts",
+ "build": "pnpm run codegen && svelte-package --input src",
+ "typecheck": "svelte-check --tsconfig ./tsconfig.check.json",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "prepublishOnly": "pnpm run clean && pnpm run build && pnpm run test"
+ },
+ "devDependencies": {
+ "@sveltejs/package": "^2.5.8",
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/svelte": "^5.2.8",
+ "@types/node": "^26.1.0",
+ "jsdom": "^29.1.1",
+ "svelte": "^5.56.10",
+ "svelte-check": "^4.7.6",
+ "tsx": "^4.23.0",
+ "typescript": "^6.0.3",
+ "vite": "^8.1.3",
+ "vitest": "^4.1.10"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0"
+ }
+}
diff --git a/packages/svelte/scripts/__tests__/extractName.test.ts b/packages/svelte/scripts/__tests__/extractName.test.ts
new file mode 100644
index 00000000..4f2f0e9e
--- /dev/null
+++ b/packages/svelte/scripts/__tests__/extractName.test.ts
@@ -0,0 +1,73 @@
+import { describe, it, expect } from 'vitest'
+import { extractName, toPascalCase } from '../extractName'
+
+describe('extractName', () => {
+ it('extracts a single-segment name', () => {
+ expect(extractName('dashboard.svg')).toEqual({
+ name: 'dashboard',
+ componentName: 'Dashboard',
+ aliasName: 'DashboardIcon',
+ })
+ })
+
+ it('PascalCases multi-segment kebab names', () => {
+ expect(extractName('arrow-up.svg')).toMatchObject({
+ name: 'arrow-up',
+ componentName: 'ArrowUp',
+ aliasName: 'ArrowUpIcon',
+ })
+ expect(extractName('arrow-autofit-content-filled.svg')).toMatchObject({
+ componentName: 'ArrowAutofitContentFilled',
+ })
+ })
+
+ it('allows trailing digits in segments', () => {
+ expect(extractName('trash-2.svg')).toMatchObject({
+ componentName: 'Trash2',
+ aliasName: 'Trash2Icon',
+ })
+ expect(extractName('h2.svg')).toMatchObject({ componentName: 'H2' })
+ })
+
+ it('rejects filenames without the .svg extension', () => {
+ expect(() => extractName('dashboard.png')).toThrow(/Expected an .svg/)
+ })
+
+ it('rejects names starting with a digit (invalid JS identifier)', () => {
+ expect(() => extractName('2arrow.svg')).toThrow(/digit/)
+ })
+
+ it('rejects names containing invalid characters', () => {
+ expect(() => extractName('Bad_Name.svg')).toThrow(/Invalid icon filename/)
+ expect(() => extractName('arrow up.svg')).toThrow(/Invalid icon filename/)
+ expect(() => extractName('-arrow.svg')).toThrow(/Invalid icon filename/)
+ expect(() => extractName('arrow-.svg')).toThrow(/Invalid icon filename/)
+ })
+
+ it('rejects empty names', () => {
+ expect(() => extractName('.svg')).toThrow()
+ })
+})
+
+describe('toPascalCase', () => {
+ it('handles single words', () => {
+ expect(toPascalCase('star')).toBe('Star')
+ })
+
+ it('handles multi-segment kebab', () => {
+ expect(toPascalCase('arrow-autofit-down')).toBe('ArrowAutofitDown')
+ })
+
+ it('preserves digits within segments', () => {
+ expect(toPascalCase('h2')).toBe('H2')
+ expect(toPascalCase('trash-2')).toBe('Trash2')
+ })
+
+ it('disambiguates adjacent digit-only segments with an underscore', () => {
+ expect(toPascalCase('ironing-1-1')).toBe('Ironing1_1')
+ expect(toPascalCase('ironing-11')).toBe('Ironing11')
+ expect(toPascalCase('a-1-2-3')).toBe('A1_2_3')
+ expect(toPascalCase('a-1-b')).toBe('A1B')
+ expect(toPascalCase('a-b-1')).toBe('AB1')
+ })
+})
diff --git a/packages/svelte/scripts/__tests__/transformSvg.test.ts b/packages/svelte/scripts/__tests__/transformSvg.test.ts
new file mode 100644
index 00000000..5bceebf3
--- /dev/null
+++ b/packages/svelte/scripts/__tests__/transformSvg.test.ts
@@ -0,0 +1,144 @@
+import { describe, it, expect } from 'vitest'
+import { transformSvg } from '../transformSvg'
+
+const wrap = (inner: string, attrs = 'xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"') =>
+ ``
+
+describe('transformSvg', () => {
+ it('extracts inner content and returns the viewBox', () => {
+ const result = transformSvg(wrap(''), 'sample')
+ expect(result.viewBox).toBe('0 0 24 24')
+ expect(result.innerSvg).toBe('')
+ })
+
+ it('lifts presentation attrs from the source