Add Svelte 5 (Runes) icon package (#1434)

Introduced the @tabler/icons-svelte-runes package for Svelte 5+ with runes reactivity, including source, build, tests, and documentation. Updated issue templates, labeler, and README to reference the new package and distinguish between Svelte 4 and Svelte 5 usage. Adjusted build scripts and lockfile to support the new package.
This commit is contained in:
Abdul-Kadir Coskun 2025-12-14 23:03:48 +11:00 committed by GitHub
parent 48280d7eb6
commit 0678fad12c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1526 additions and 7 deletions

View File

@ -31,6 +31,7 @@ body:
- label: "@tabler/icons-react-native"
- label: "@tabler/icons-solid"
- label: "@tabler/icons-svelte"
- label: "@tabler/icons-svelte-runes"
- label: "@tabler/icons-vue"
- label: Figma plugin
- label: source/main

8
.github/labeler.yml vendored
View File

@ -38,12 +38,18 @@
- any-glob-to-any-file:
- 'packages/icons-preact/*'
# For Svelte package
# For Svelte 4 and below package
🔗 svelte package:
- changed-files:
- any-glob-to-any-file:
- 'packages/icons-svelte/*'
# For Svelte 5 package
🔗 svelte-runes package:
- changed-files:
- any-glob-to-any-file:
- 'packages/icons-svelte-runes/*'
# For SolidJS package
🔗 solid package:
- changed-files:

View File

@ -232,7 +232,7 @@ After importing the _IconsModule_ in your feature or shared module, use the icon
For more usage documentation refer to [the official documentation](https://github.com/pierreavn/angular-tabler-icons).
### Svelte
### Svelte 4 and below
Svelte components available through [`@tabler/icons-svelte`](https://github.com/tabler/tabler-icons/tree/master/packages/icons-svelte) package.
@ -248,6 +248,21 @@ Svelte components available through [`@tabler/icons-svelte`](https://github.com/
</main>
```
### Svelte 5
Svelte 5 components available through [`@tabler/icons-svelte-runes`](https://www.npmjs.com/package/@tabler/icons-svelte-runes) package.
```js
<script lang="ts">
import { IconHeart } from '@tabler/icons-svelte-runes';
</script>
<main>
<IconHeart size={48} stroke={1} />
<IconHeart size="32" stroke={1.5} />
<IconHeart color="crimson" class="p-1" size="96" stroke="2" />
</main>
```
## CDN
All files included in `@tabler/icons` npm package are available over a CDN.

View File

@ -0,0 +1,40 @@
---
title: Tabler Icons for Svelte 5
---
![](https://raw.githubusercontent.com/tabler/tabler-icons/master/.github/packages/og-package-svelte.png)
## Installation
<TabsPackage name="@tabler/icons-svelte-runes" />
or just [download from Github](https://github.com/tabler/tabler-icons/releases).
## How to use
It's build with ESmodules so it's completely tree-shakable. Each icon can be imported as a component.
```sveltehtml
<script lang="ts">
import { IconHeart } from '@tabler/icons-svelte-runes';
</script>
<main>
<IconHeart />
</main>
```
You can pass additional props to adjust the icon.
```html
<IconHeart size={48} stroke={1} />
```
### Props
| name | type | default |
| ------------- | -------- | ------------ |
| `size` | _Number_ | 24 |
| `color` | _String_ | currentColor |
| `stroke` | _Number_ | 2 |
| `class` | _String_ | |

View File

@ -33,13 +33,14 @@
"validate": "node ./.build/validate-icons.mjs",
"release": "git pull && release-it --verbose",
"build:copy": "rm -rf ./icons && mkdir ./icons && cp ./_site/tags.json tags.json && cp ./_site/icons/* ./icons/ && rm -rf ./_site/",
"build:packages": "pnpm run build:icons && pnpm run build:sprite && pnpm run build:react && pnpm run build:react-native && pnpm run build:preact && pnpm run build:solidjs && pnpm run build:svelte && pnpm run build:vue && pnpm run build:png && pnpm run build:pdf && pnpm run build:esp && pnpm run build:webfont",
"build:packages": "pnpm run build:icons && pnpm run build:sprite && pnpm run build:react && pnpm run build:react-native && pnpm run build:preact && pnpm run build:solidjs && pnpm run build:svelte && pnpm run build:svelte-runes && pnpm run build:vue && pnpm run build:png && pnpm run build:pdf && pnpm run build:esp && pnpm run build:webfont",
"build:icons": "pnpm --filter @tabler/icons build",
"build:react": "pnpm --filter @tabler/icons-react build",
"build:react-native": "pnpm --filter @tabler/icons-react-native build",
"build:preact": "pnpm --filter @tabler/icons-preact build",
"build:solidjs": "pnpm --filter @tabler/icons-solidjs build",
"build:svelte": "pnpm --filter @tabler/icons-svelte build",
"build:svelte-runes": "pnpm --filter @tabler/icons-svelte-runes build",
"build:vue": "pnpm --filter @tabler/icons-vue build",
"build:png": "pnpm --filter @tabler/icons-png build",
"build:pdf": "pnpm --filter @tabler/icons-pdf build",

View File

@ -0,0 +1,4 @@
src/icons/*.svelte
.svelte-kit
src/aliases.ts
src/icons-list.ts

View File

@ -0,0 +1,207 @@
# Tabler Icons for Svelte (Runes)
<p align="center">
<img src="https://raw.githubusercontent.com/tabler/tabler-icons/main/.github/packages/og-package-svelte.png" alt="Tabler Icons" width="838">
</p>
<p align="center">
Implementation of the Tabler Icons library for Svelte 5+ using the new runes reactivity system.
<p>
<p align="center">
<a href="https://tabler-icons.io/"><strong>Browse all icons at tabler-icons.io &rarr;</strong></a>
</p>
<p align="center">
<a href="https://github.com/tabler/tabler-icons/releases"><img src="https://img.shields.io/npm/v/@tabler/icons-svelte-runes" alt="Latest Release"></a>
<a href="https://github.com/tabler/tabler-icons/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@tabler/icons.svg" alt="License"></a>
</p>
## Why This Package?
This package is specifically built for **Svelte 5+** using the new **runes** reactivity system (`$props()`, `$derived`, etc.).
- **For Svelte 5+ projects:** Use this package (`@tabler/icons-svelte-runes`)
- **For Svelte 3/4 projects:** Use [`@tabler/icons-svelte`](https://www.npmjs.com/package/@tabler/icons-svelte)
## Installation
```bash
pnpm add @tabler/icons-svelte-runes
```
or
```bash
npm install @tabler/icons-svelte-runes
```
or
```bash
yarn add @tabler/icons-svelte-runes
```
## Requirements
- **Svelte 5.0+** with runes enabled
- For older Svelte versions, use `@tabler/icons-svelte` instead
## Usage
It's built with ES modules so it's completely tree-shakable. Each icon can be imported as a component.
```svelte
<script lang="ts">
import { IconHeart } from '@tabler/icons-svelte-runes';
</script>
<IconHeart />
```
You can pass additional props to adjust the icon:
```svelte
<IconHeart size={48} stroke={1} color="red" />
```
### Props
| name | type | default |
| -------- | ------------------ | ------------ |
| `size` | `number \| string` | 24 |
| `color` | `string` | currentColor |
| `stroke` | `number \| string` | 2 |
| `class` | `string` | '' |
All other HTML attributes are forwarded to the SVG element.
## TypeScript Support
The package includes full TypeScript definitions. Icons are typed as Svelte 5 `Component<IconProps>`:
```typescript
import type { Icon } from '@tabler/icons-svelte-runes';
import { IconHeart } from '@tabler/icons-svelte-runes';
// Icon is compatible with Svelte 5's Component type
const MyIcon: Icon = IconHeart;
```
### Using Icons in Props
When passing icons as props, use the `Component` type or `any` for maximum compatibility:
```svelte
<script lang="ts">
import type { Component } from 'svelte';
import { IconHeart } from '@tabler/icons-svelte-runes';
interface Props {
icon: Component<any>;
label: string;
}
let { icon: Icon, label }: Props = $props();
</script>
<button>
<Icon size={20} />
{label}
</button>
```
## Examples
### Basic Icon Usage
```svelte
<script lang="ts">
import { IconHeart, IconStar, IconHome } from '@tabler/icons-svelte-runes';
</script>
<IconHeart />
<IconStar size={32} color="gold" />
<IconHome stroke={1.5} class="my-icon" />
```
### Dynamic Icons with Runes
```svelte
<script lang="ts">
import { IconHeart, IconStar, IconCircle } from '@tabler/icons-svelte-runes';
const icons = {
heart: IconHeart,
star: IconStar,
circle: IconCircle
};
let selected = $state('heart');
let DynamicIcon = $derived(icons[selected]);
</script>
<DynamicIcon size={32} />
<button onclick={() => selected = 'heart'}>Heart</button>
<button onclick={() => selected = 'star'}>Star</button>
<button onclick={() => selected = 'circle'}>Circle</button>
```
### Reactive Size with $derived
```svelte
<script lang="ts">
import { IconHeart } from '@tabler/icons-svelte-runes';
let isLarge = $state(false);
let iconSize = $derived(isLarge ? 48 : 24);
</script>
<IconHeart size={iconSize} />
<button onclick={() => isLarge = !isLarge}>Toggle Size</button>
```
## Migrating from @tabler/icons-svelte
The API is identical, just change the package name:
```diff
- import { IconHeart } from '@tabler/icons-svelte';
+ import { IconHeart } from '@tabler/icons-svelte-runes';
```
No other changes needed! Your existing props and usage remain the same.
## What's Different from @tabler/icons-svelte?
Internally, this package uses Svelte 5's new features:
- `$props()` instead of `export let`
- `$derived` for computed values
- Modern TypeScript `Component` type
- Optimized for Svelte 5's fine-grained reactivity
**Result:** Better performance and smaller bundle sizes in Svelte 5 projects!
## Sponsors
**If you want to support this project, you can [become a sponsor on GitHub](https://github.com/sponsors/codecalm) or just [donate on PayPal](https://paypal.me/codecalm) :)**
<a href="https://github.com/sponsors/codecalm">
<img src="https://cdn.jsdelivr.net/gh/tabler/sponsors@latest/sponsors.svg" alt="Tabler sponsors">
</a>
## Contributing
For more info on how to contribute please see the [contribution guidelines](https://github.com/tabler/tabler-icons/blob/main/CONTRIBUTING.md).
Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/tabler/tabler-icons/blob/main/packages/icons-svelte-runes/README.md)
## License
Tabler Icons is licensed under the [MIT License](https://github.com/tabler/tabler-icons/blob/master/LICENSE).
## Sponsor Tabler
<a href="https://github.com/sponsors/codecalm" target="_blank"><img src="https://github.com/tabler/tabler/raw/dev/src/static/sponsor-banner-readme.png?raw=true" alt="Sponsor Tabler" /></a>

View File

@ -0,0 +1,41 @@
#!/usr/bin/env node
import { buildJsIcons, buildIconsList } from '../../.build/build-icons.mjs';
const componentTemplate = ({ type, name, children, stringify }) => {
return `\
<script lang="ts">
import Icon from '../Icon.svelte';
import type { IconNode, IconProps } from '../types.js';
import type { Snippet } from 'svelte';
interface Props extends IconProps {
children?: Snippet;
}
let { children, ...props }: Props = $props();
const iconNode: IconNode = ${JSON.stringify(children)};
</script>
<Icon type="${type}" name="${name}" {...props} iconNode={iconNode} {children} />
`;
};
const aliasTemplate = ({ fromPascal, to }) =>
`export { default as Icon${fromPascal} } from './icons/${to}.svelte';\n`;
const indexItemTemplate = ({ name, namePascal }) =>
`export { default as Icon${namePascal} } from './${name}.svelte';`;
buildJsIcons({
name: 'icons-svelte-runes',
componentTemplate,
indexItemTemplate,
aliasTemplate,
extension: 'svelte',
key: false,
indexFile: 'index.ts',
pascalName: false,
});
buildIconsList('icons-svelte-runes');

View File

@ -0,0 +1,83 @@
{
"name": "@tabler/icons-svelte-runes",
"version": "1.0.0",
"license": "MIT",
"author": "codecalm",
"description": "A set of free MIT-licensed high-quality SVG icons for Svelte 5+ using runes",
"homepage": "https://tabler-icons.io",
"bugs": {
"url": "https://github.com/tabler/tabler-icons/issues"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tabler/tabler-icons.git",
"directory": "packages/icons-svelte-runes"
},
"svelte": "./dist/tabler-icons-svelte-runes.js",
"types": "./dist/tabler-icons-svelte-runes.d.ts",
"type": "module",
"amdName": "TablerIconsSvelteRunes",
"exports": {
".": {
"types": "./dist/tabler-icons-svelte-runes.d.ts",
"svelte": "./dist/tabler-icons-svelte-runes.js",
"default": "./dist/tabler-icons-svelte-runes.js"
},
"./icons": {
"types": "./dist/tabler-icons-svelte-runes.d.ts",
"svelte": "./dist/tabler-icons-svelte-runes.js"
},
"./icons/*": {
"types": "./dist/icons/*.svelte.d.ts",
"svelte": "./dist/icons/*.svelte"
}
},
"sideEffects": false,
"files": [
"dist"
],
"keywords": [
"svelte",
"svelte5",
"runes",
"icons",
"svg",
"tabler",
"tabler-icons",
"svelte-runes",
"ui"
],
"scripts": {
"build": "pnpm run clean && pnpm run copy:license && pnpm run build:icons && pnpm run build:package",
"build:icons": "node build.mjs",
"build:package": "svelte-package --input ./src",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && find . ! -name '.gitkeep' -path '*/src/icons/*' -exec rm -rf {} +",
"test": "vitest run",
"imports-check": "attw $(npm pack)"
},
"dependencies": {
"@tabler/icons": "3.35.0"
},
"devDependencies": {
"@sveltejs/package": "^2.3.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.9",
"@tsconfig/svelte": "^5.0.4",
"jest-serializer-html": "^7.1.0",
"jsdom": "^25.0.1",
"svelte": "^5.0.0",
"svelte-check": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^7.2.7",
"vitest": "^3.0.8"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
}

View File

@ -0,0 +1,9 @@
import { expect } from 'vitest';
import * as matchers from '@testing-library/jest-dom/matchers';
import htmlSerializer from 'jest-serializer-html';
// Extend Vitest's expect with jest-dom matchers
expect.extend(matchers);
// Add HTML serializer for snapshots
expect.addSnapshotSerializer(htmlSerializer);

View File

@ -0,0 +1,53 @@
<script lang="ts">
import defaultAttributes from './defaultAttributes';
import type { IconNode } from './types';
import type { Snippet } from 'svelte';
interface Props {
type: 'outline' | 'filled';
name: string;
color?: string;
size?: number | string;
stroke?: number | string;
iconNode: IconNode;
class?: string;
children?: Snippet;
[key: string]: any; // For rest props
}
let {
type,
name,
color = 'currentColor',
size = 24,
stroke = 2,
iconNode,
class: className = '',
children,
...restProps
}: Props = $props();
// Derive the fill/stroke attributes based on type
const typeAttributes = $derived(
type === 'filled' ? { fill: color } : { 'stroke-width': stroke, stroke: color },
);
// Derive the full class name
const fullClassName = $derived(`tabler-icon tabler-icon-${name} ${className}`);
</script>
<svg
{...defaultAttributes[type]}
{...restProps}
width={size}
height={size}
class={fullClassName}
{...typeAttributes}
>
{#each iconNode as [tag, attrs]}
<svelte:element this={tag} {...attrs} />
{/each}
{#if children}
{@render children()}
{/if}
</svg>

View File

@ -0,0 +1,25 @@
import type { Attrs } from "./types.js";
const defaultAttributes: Record<"outline" | "filled", Attrs> = {
outline: {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
},
filled: {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'currentColor',
stroke: 'none'
},
};
export default defaultAttributes;

View File

@ -0,0 +1,7 @@
export * from './icons/index.js';
export * as icons from './icons/index.js';
export * as iconsList from './icons-list.js';
export * from './aliases.js';
export { default as defaultAttributes } from './defaultAttributes.js';
export type { Icon } from './types.js';

View File

@ -0,0 +1,17 @@
import type { Component, Snippet } from 'svelte';
import type { SVGAttributes, SvelteHTMLElements } from 'svelte/elements';
export type Attrs = SVGAttributes<SVGSVGElement>;
export type IconNode = [elementName: keyof SvelteHTMLElements, attrs: Attrs][];
export interface IconProps extends Omit<Attrs, 'color' | 'stroke'> {
color?: string;
size?: number | string;
stroke?: number | string;
class?: string;
children?: Snippet;
}
// Svelte 5 compatible icon type
export type Icon = Component<IconProps>;

View File

@ -0,0 +1,8 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
compilerOptions: {
runes: true,
},
};

View File

@ -0,0 +1,132 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/svelte';
import { IconAccessible, IconAccessibleFilled } from './src/tabler-icons-svelte-runes.js';
describe('Svelte Runes Icon component', () => {
afterEach(() => cleanup());
it('should render icon component', () => {
const { container } = render(IconAccessible);
expect(container.getElementsByTagName('svg').length).toBeGreaterThan(0);
});
it('should add a class to the element', () => {
const { container } = render(IconAccessible, {
props: {
class: 'test-class',
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg).toHaveClass('test-class');
expect(svg).toHaveClass('tabler-icon');
expect(svg).toHaveClass('tabler-icon-accessible');
});
it('should add a style attribute to the element', () => {
const { container } = render(IconAccessible, {
props: {
style: 'color: red',
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg).toHaveStyle('color: rgb(255, 0, 0)');
});
it('should update svg attributes when there are props passed to the component', () => {
const { container } = render(IconAccessible, {
props: {
size: 48,
color: 'red',
stroke: 4,
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg.getAttribute('width')).toBe('48');
expect(svg.getAttribute('stroke')).toBe('red');
expect(svg.getAttribute('stroke-width')).toBe('4');
});
it('should accept stroke as a number', () => {
const { container } = render(IconAccessible, {
props: {
stroke: 3,
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg.getAttribute('stroke-width')).toBe('3');
});
it('should accept size as a string', () => {
const { container } = render(IconAccessible, {
props: {
size: '100%',
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg.getAttribute('width')).toBe('100%');
expect(svg.getAttribute('height')).toBe('100%');
});
it('should update svg attributes when there are props passed to the filled version of component', () => {
const { container } = render(IconAccessibleFilled, {
props: {
size: 48,
color: 'red',
},
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg.getAttribute('width')).toBe('48');
expect(svg.getAttribute('fill')).toBe('red');
expect(svg.getAttribute('stroke')).toBe('none');
expect(svg.getAttribute('stroke-width')).toBe(null);
});
it('should render children content using snippets', () => {
const { container } = render(IconAccessible, {
props: {},
// Test that children can be passed (even though we can't easily test snippet content in this setup)
});
const svg = container.getElementsByTagName('svg')[0];
expect(svg).toBeDefined();
});
it('should match snapshot', () => {
const { container } = render(IconAccessible);
expect(container.innerHTML).toMatchInlineSnapshot(`
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewbox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="tabler-icon tabler-icon-accessible "
>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0">
</path>
<path d="M10 16.5l2 -3l2 3m-2 -3v-2l3 -1m-6 0l3 1">
</path>
<circle cx="12"
cy="7.5"
r=".5"
fill="currentColor"
>
</circle>
</svg>
`);
});
});

View File

@ -0,0 +1,27 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"strict": true,
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": [
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte",
"tests/**/*.ts",
"*.spec.js",
"vitest-setup.d.ts"
],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,7 @@
/// <reference types="vitest" />
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
declare module 'vitest' {
interface Assertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
interface AsymmetricMatchersContaining extends TestingLibraryMatchers<any, void> {}
}

View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './setupVitest.ts',
},
resolve: {
conditions: ['browser'],
},
});

File diff suppressed because it is too large Load Diff