diff --git a/.prettierignore b/.prettierignore
index 4a40c2a72e..c150f271bb 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,6 +2,10 @@ pnpm-lock.yaml
# docs examples
docs/**/examples/
+docs/.vitepress/.temp
+docs/.vitepress/cache
+docs/.vitepress/data
+docs/.nitro
# lucide-angular
packages/lucide-angular/.angular/cache
diff --git a/docs/guide/packages/lucide-angular.md b/docs/guide/packages/lucide-angular.md
index aba4524502..4999f3368d 100644
--- a/docs/guide/packages/lucide-angular.md
+++ b/docs/guide/packages/lucide-angular.md
@@ -115,3 +115,20 @@ import { icons } from 'lucide-angular';
LucideAngularModule.pick(icons)
```
+
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+They can be used in the same way as the official icons.
+
+```js
+import { LucideAngularModule } from 'lucide-angular';
+import { burger } from '@lucide/lab';
+
+@NgModule({
+ imports: [
+ LucideAngularModule.pick({ burger })
+ ]
+})
+export class AppModule { }
+```
diff --git a/docs/guide/packages/lucide-preact.md b/docs/guide/packages/lucide-preact.md
index 6fd69b9af4..7ebb879e2c 100644
--- a/docs/guide/packages/lucide-preact.md
+++ b/docs/guide/packages/lucide-preact.md
@@ -67,6 +67,26 @@ const App = () => {
> SVG attributes in Preact aren't transformed, so if you want to change for example the `stroke-linejoin` you need to pass it in kebabcase. Basically how the SVG spec want you to write it. See this topic in the [Preact documentation](https://preactjs.com/guide/v10/differences-to-react/#svg-inside-jsx).
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like regular lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```jsx
+import { Icon } from 'lucide-preact';
+import { burger } from '@lucide/lab';
+
+const App = () => (
+
+);
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons, but it is not recommended.
diff --git a/docs/guide/packages/lucide-react-native.md b/docs/guide/packages/lucide-react-native.md
index 112e33230e..c5dacbe00d 100644
--- a/docs/guide/packages/lucide-react-native.md
+++ b/docs/guide/packages/lucide-react-native.md
@@ -61,6 +61,26 @@ const App = () => {
};
```
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like regular lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```jsx
+import { Icon } from 'lucide-react-native';
+import { burger } from '@lucide/lab';
+
+const App = () => (
+
+);
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons, but it is not recommended.
diff --git a/docs/guide/packages/lucide-react.md b/docs/guide/packages/lucide-react.md
index 87f78f3b98..4c8a0c303a 100644
--- a/docs/guide/packages/lucide-react.md
+++ b/docs/guide/packages/lucide-react.md
@@ -61,6 +61,26 @@ const App = () => {
};
```
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like regular lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```jsx
+import { Icon } from 'lucide-react';
+import { burger } from '@lucide/lab';
+
+const App = () => (
+
+);
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons, but it is not recommended.
diff --git a/docs/guide/packages/lucide-solid.md b/docs/guide/packages/lucide-solid.md
index 94087df819..119f51d620 100644
--- a/docs/guide/packages/lucide-solid.md
+++ b/docs/guide/packages/lucide-solid.md
@@ -61,6 +61,26 @@ const App = () => {
};
```
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like the regular Lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```jsx
+import { Icon } from 'lucide-solid';
+import { burger, sausage } from '@lucide/lab';
+
+const App = () => (
+
+);
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons. It's not recommended.
diff --git a/docs/guide/packages/lucide-svelte.md b/docs/guide/packages/lucide-svelte.md
index 184aa2076c..2077a12a86 100644
--- a/docs/guide/packages/lucide-svelte.md
+++ b/docs/guide/packages/lucide-svelte.md
@@ -166,6 +166,27 @@ The package includes type definitions for all icons. This is useful if you want
For more details about typing the `svelte:component` directive, see the [Svelte documentation](https://svelte.dev/docs/typescript#types-componenttype).
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like the regular Lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```svelte
+
+
+
+
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons, but it is not recommended.
diff --git a/docs/guide/packages/lucide-vue-next.md b/docs/guide/packages/lucide-vue-next.md
index acf1c2448b..367a4aafd4 100644
--- a/docs/guide/packages/lucide-vue-next.md
+++ b/docs/guide/packages/lucide-vue-next.md
@@ -37,16 +37,16 @@ Each icon can be imported as a Vue component, which renders an inline SVG Elemen
You can pass additional props to adjust the icon.
```vue
+
+
-
-
```
## Props
@@ -69,6 +69,28 @@ To customize the appearance of an icon, you can pass custom properties as props
```
+## With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+
+They can be used by using the `Icon` component.
+All props like regular lucide icons can be passed to adjust the icon appearance.
+
+### Using the `Icon` component
+
+This creates a single icon based on the iconNode passed and renders a Lucide icon component.
+
+```vue
+
+
+
+
+
+```
+
## One generic icon component
It is possible to create one generic icon component to load icons, but it is not recommended.
diff --git a/docs/guide/packages/lucide.md b/docs/guide/packages/lucide.md
index 068917ce8c..c42b228ac9 100644
--- a/docs/guide/packages/lucide.md
+++ b/docs/guide/packages/lucide.md
@@ -130,3 +130,18 @@ menuIcon.classList.add('my-icon-class');
const myApp = document.getElementById('app');
myApp.appendChild(menuIcon);
```
+
+### With Lucide lab or custom icons
+
+[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
+They can be used in the same way as the official icons.
+
+```js
+import { burger } from '@lucide/lab';
+
+createIcons({
+ icons: {
+ burger
+ }
+});
+```
diff --git a/packages/lucide-preact/src/Icon.ts b/packages/lucide-preact/src/Icon.ts
new file mode 100644
index 0000000000..bfbda22977
--- /dev/null
+++ b/packages/lucide-preact/src/Icon.ts
@@ -0,0 +1,50 @@
+import { h, toChildArray } from 'preact';
+import defaultAttributes from './defaultAttributes';
+import type { IconNode, LucideProps } from './types';
+
+interface IconComponentProps extends LucideProps {
+ iconNode: IconNode;
+}
+
+/**
+ * Lucide icon component
+ *
+ * @component Icon
+ * @param {object} props
+ * @param {string} props.color - The color of the icon
+ * @param {number} props.size - The size of the icon
+ * @param {number} props.strokeWidth - The stroke width of the icon
+ * @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
+ * @param {string} props.class - The class name of the icon
+ * @param {IconNode} props.children - The children of the icon
+ * @param {IconNode} props.iconNode - The icon node of the icon
+ *
+ * @returns {ForwardRefExoticComponent} LucideIcon
+ */
+const Icon = ({
+ color = 'currentColor',
+ size = 24,
+ strokeWidth = 2,
+ absoluteStrokeWidth,
+ children,
+ iconNode,
+ class: classes = '',
+ ...rest
+}: IconComponentProps) =>
+ h(
+ 'svg',
+ {
+ ...defaultAttributes,
+ width: String(size),
+ height: size,
+ stroke: color,
+ ['stroke-width' as 'strokeWidth']: absoluteStrokeWidth
+ ? (Number(strokeWidth) * 24) / Number(size)
+ : strokeWidth,
+ class: ['lucide', classes].join(' '),
+ ...rest,
+ },
+ [...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)],
+ );
+
+export default Icon;
diff --git a/packages/lucide-preact/src/createLucideIcon.ts b/packages/lucide-preact/src/createLucideIcon.ts
index e4eb95f3b6..2ba13086f3 100644
--- a/packages/lucide-preact/src/createLucideIcon.ts
+++ b/packages/lucide-preact/src/createLucideIcon.ts
@@ -1,17 +1,7 @@
-import { type FunctionComponent, h, type JSX, toChildArray } from 'preact';
-import defaultAttributes from './defaultAttributes';
-import { toKebabCase } from '@lucide/shared';
-
-export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record][];
-
-export interface LucideProps extends Partial> {
- color?: string;
- size?: string | number;
- strokeWidth?: string | number;
- absoluteStrokeWidth?: boolean;
-}
-
-export type LucideIcon = FunctionComponent;
+import { h, type JSX } from 'preact';
+import { mergeClasses, toKebabCase } from '@lucide/shared';
+import Icon from './Icon';
+import type { IconNode, LucideIcon, LucideProps } from './types';
/**
* Create a Lucide icon component
@@ -20,29 +10,18 @@ export type LucideIcon = FunctionComponent;
* @returns {FunctionComponent} LucideIcon
*/
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
- const Component = ({
- color = 'currentColor',
- size = 24,
- strokeWidth = 2,
- absoluteStrokeWidth,
- children,
- class: classes = '',
- ...rest
- }: LucideProps) =>
+ const Component = ({ class: classes = '', children, ...props }: LucideProps) =>
h(
- 'svg',
+ Icon,
{
- ...defaultAttributes,
- width: String(size),
- height: size,
- stroke: color,
- ['stroke-width' as 'strokeWidth']: absoluteStrokeWidth
- ? (Number(strokeWidth) * 24) / Number(size)
- : strokeWidth,
- class: ['lucide', `lucide-${toKebabCase(iconName)}`, classes].join(' '),
- ...rest,
+ ...props,
+ iconNode,
+ class: mergeClasses>(
+ `lucide-${toKebabCase(iconName)}`,
+ classes,
+ ),
},
- [...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)],
+ children,
);
Component.displayName = `${iconName}`;
diff --git a/packages/lucide-preact/src/lucide-preact.ts b/packages/lucide-preact/src/lucide-preact.ts
index 2ddd5eb314..6636c22592 100644
--- a/packages/lucide-preact/src/lucide-preact.ts
+++ b/packages/lucide-preact/src/lucide-preact.ts
@@ -1,4 +1,7 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
+export * from './types';
+
export { default as createLucideIcon } from './createLucideIcon';
+export { default as Icon } from './Icon';
diff --git a/packages/lucide-preact/src/types.ts b/packages/lucide-preact/src/types.ts
new file mode 100644
index 0000000000..03604e9606
--- /dev/null
+++ b/packages/lucide-preact/src/types.ts
@@ -0,0 +1,12 @@
+import { type FunctionComponent, type JSX } from 'preact';
+
+export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record][];
+
+export interface LucideProps extends Partial> {
+ color?: string;
+ size?: string | number;
+ strokeWidth?: string | number;
+ absoluteStrokeWidth?: boolean;
+}
+
+export type LucideIcon = FunctionComponent;
diff --git a/packages/lucide-preact/tests/Icon.spec.tsx b/packages/lucide-preact/tests/Icon.spec.tsx
new file mode 100644
index 0000000000..d43760ba93
--- /dev/null
+++ b/packages/lucide-preact/tests/Icon.spec.tsx
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/preact';
+
+import { airVent } from './testIconNodes';
+import { Icon } from '../src/lucide-preact';
+
+describe('Using Icon Component', () => {
+ it('should render icon based on a iconNode', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap
new file mode 100644
index 0000000000..2e30bf9a62
--- /dev/null
+++ b/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap
@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using Icon Component > should render icon and match snapshot 1`] = `
+
+`;
diff --git a/packages/lucide-preact/tests/__snapshots__/createLucideIcon.spec.tsx.snap b/packages/lucide-preact/tests/__snapshots__/createLucideIcon.spec.tsx.snap
new file mode 100644
index 0000000000..acf4f97390
--- /dev/null
+++ b/packages/lucide-preact/tests/__snapshots__/createLucideIcon.spec.tsx.snap
@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using createLucideIcon > should create a component from an iconNode 1`] = `
+
+`;
diff --git a/packages/lucide-preact/tests/__snapshots__/lucide-preact.spec.tsx.snap b/packages/lucide-preact/tests/__snapshots__/lucide-preact.spec.tsx.snap
index 6df3af2c0f..a3cc086577 100644
--- a/packages/lucide-preact/tests/__snapshots__/lucide-preact.spec.tsx.snap
+++ b/packages/lucide-preact/tests/__snapshots__/lucide-preact.spec.tsx.snap
@@ -10,8 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
- data-testid="grid-icon"
+ class="lucide lucide-grid3x3"
>
should not scale the strokeWidth when ab
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
- data-testid="grid-icon"
+ class="lucide lucide-grid3x3"
>
should render an component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
+ class="lucide lucide-grid3x3"
>
{
+ it('should create a component from an iconNode', () => {
+ const AirVent = createLucideIcon('AirVent', airVent);
+
+ const { container } = render();
+
+ expect(container.firstChild).toMatchSnapshot();
+ expect(container.firstChild).toBeDefined();
+ });
+});
diff --git a/packages/lucide-preact/tests/lucide-preact.spec.tsx b/packages/lucide-preact/tests/lucide-preact.spec.tsx
index c0e6d06eb9..819ebfe728 100644
--- a/packages/lucide-preact/tests/lucide-preact.spec.tsx
+++ b/packages/lucide-preact/tests/lucide-preact.spec.tsx
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';
import { render, cleanup } from '@testing-library/preact';
import { Pen, Edit2, Grid, Droplet } from '../src/lucide-preact';
+import defaultAttributes from '../src/defaultAttributes';
type AttributesAssertion = { attributes: Record };
@@ -11,30 +12,43 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
+ it('should render the icon with the default attributes', () => {
+ const { container } = render();
+
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('xmlns', defaultAttributes.xmlns);
+ expect(SVGElement).toHaveAttribute('width', String(defaultAttributes.width));
+ expect(SVGElement).toHaveAttribute('height', String(defaultAttributes.height));
+ expect(SVGElement).toHaveAttribute('viewBox', defaultAttributes.viewBox);
+ expect(SVGElement).toHaveAttribute('fill', defaultAttributes.fill);
+ expect(SVGElement).toHaveAttribute('stroke', defaultAttributes.stroke);
+ expect(SVGElement).toHaveAttribute('stroke-width', String(defaultAttributes['stroke-width']));
+ expect(SVGElement).toHaveAttribute('stroke-linecap', defaultAttributes['stroke-linecap']);
+ expect(SVGElement).toHaveAttribute('stroke-linejoin', defaultAttributes['stroke-linejoin']);
+ });
+
it('should adjust the size, stroke color and stroke width', () => {
- const testId = 'grid-icon';
- const { container, getByTestId } = render(
+ const { container } = render(
,
);
- const { attributes } = getByTestId(testId) as unknown as AttributesAssertion;
- expect(attributes.stroke.value).toBe('red');
- expect(attributes.width.value).toBe('48');
- expect(attributes.height.value).toBe('48');
- expect(attributes['stroke-width'].value).toBe('4');
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '4');
expect(container.innerHTML).toMatchSnapshot();
});
it('should render the alias icon', () => {
- const testId = 'pen-icon';
const { container } = render(
{
const { container: Edit2Container } = render(
{
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
- const testId = 'grid-icon';
- const { container, getByTestId } = render(
+ const { container } = render(
,
);
- const { attributes } = getByTestId(testId) as unknown as AttributesAssertion;
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '1');
- expect(attributes.stroke.value).toBe('red');
- expect(attributes.width.value).toBe('48');
- expect(attributes.height.value).toBe('48');
- expect(attributes['stroke-width'].value).toBe('1');
expect(container.innerHTML).toMatchSnapshot();
});
diff --git a/packages/lucide-preact/tests/setupVitest.js b/packages/lucide-preact/tests/setupVitest.js
index ccd53195bb..4e0ce2a49d 100644
--- a/packages/lucide-preact/tests/setupVitest.js
+++ b/packages/lucide-preact/tests/setupVitest.js
@@ -1,5 +1,10 @@
-import { expect } from 'vitest';
-import '@testing-library/jest-dom';
+import { expect, afterEach } from 'vitest';
+import { cleanup } from '@testing-library/preact';
+import '@testing-library/jest-dom/vitest';
import htmlSerializer from 'jest-serializer-html';
expect.addSnapshotSerializer(htmlSerializer);
+
+afterEach(() => {
+ cleanup();
+});
diff --git a/packages/lucide-preact/tests/testIconNodes.ts b/packages/lucide-preact/tests/testIconNodes.ts
new file mode 100644
index 0000000000..8c593572d5
--- /dev/null
+++ b/packages/lucide-preact/tests/testIconNodes.ts
@@ -0,0 +1,22 @@
+import { IconNode } from '../src/createLucideIcon';
+
+export const airVent: IconNode = [
+ [
+ 'path',
+ {
+ d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
+ key: 'larmp2',
+ },
+ ],
+ ['path', { d: 'M6 8h12', key: '6g4wlu' }],
+ ['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }],
+ ['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }],
+];
+
+export const coffee: IconNode = [
+ ['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }],
+ ['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }],
+ ['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }],
+ ['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }],
+ ['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }],
+];
diff --git a/packages/lucide-react-native/package.json b/packages/lucide-react-native/package.json
index 2e3605b234..0fe54ddc9a 100644
--- a/packages/lucide-react-native/package.json
+++ b/packages/lucide-react-native/package.json
@@ -45,6 +45,7 @@
"devDependencies": {
"@lucide/rollup-plugins": "workspace:*",
"@lucide/build-icons": "workspace:*",
+ "@lucide/shared": "workspace:*",
"@testing-library/jest-dom": "^6.1.6",
"@testing-library/react": "^14.1.2",
"@types/prop-types": "^15.7.5",
diff --git a/packages/lucide-react-native/src/Icon.ts b/packages/lucide-react-native/src/Icon.ts
new file mode 100644
index 0000000000..ee43a1b0d7
--- /dev/null
+++ b/packages/lucide-react-native/src/Icon.ts
@@ -0,0 +1,69 @@
+import { createElement, forwardRef, type FunctionComponent } from 'react';
+import * as NativeSvg from 'react-native-svg';
+import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
+import { IconNode, LucideProps } from './types';
+
+interface IconComponentProps extends LucideProps {
+ iconNode: IconNode;
+}
+
+/**
+ * Lucide icon component
+ *
+ * @component Icon
+ * @param {object} props
+ * @param {string} props.color - The color of the icon
+ * @param {number} props.size - The size of the icon
+ * @param {number} props.strokeWidth - The stroke width of the icon
+ * @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
+ * @param {string} props.className - The class name of the icon
+ * @param {IconNode} props.children - The children of the icon
+ * @param {IconNode} props.iconNode - The icon node of the icon
+ *
+ * @returns {ForwardRefExoticComponent} LucideIcon
+ */
+const Icon = forwardRef(
+ (
+ {
+ color = 'currentColor',
+ size = 24,
+ strokeWidth = 2,
+ absoluteStrokeWidth,
+ children,
+ iconNode,
+ ...rest
+ },
+ ref,
+ ) => {
+ const customAttrs = {
+ stroke: color,
+ strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
+ ...rest,
+ };
+
+ return createElement(
+ NativeSvg.Svg as unknown as string,
+ {
+ ref,
+ ...defaultAttributes,
+ width: size,
+ height: size,
+ ...customAttrs,
+ },
+ [
+ ...iconNode.map(([tag, attrs]) => {
+ const upperCasedTag = (tag.charAt(0).toUpperCase() +
+ tag.slice(1)) as keyof typeof NativeSvg;
+ // duplicating the attributes here because generating the OTA update bundles don't inherit the SVG properties from parent (codepush, expo-updates)
+ return createElement(
+ NativeSvg[upperCasedTag] as FunctionComponent,
+ { ...childDefaultAttributes, ...customAttrs, ...attrs } as LucideProps,
+ );
+ }),
+ ...((Array.isArray(children) ? children : [children]) || []),
+ ],
+ );
+ },
+);
+
+export default Icon;
diff --git a/packages/lucide-react-native/src/createLucideIcon.ts b/packages/lucide-react-native/src/createLucideIcon.ts
index e824659bde..9eb27bd972 100644
--- a/packages/lucide-react-native/src/createLucideIcon.ts
+++ b/packages/lucide-react-native/src/createLucideIcon.ts
@@ -7,17 +7,7 @@ import {
} from 'react';
import * as NativeSvg from 'react-native-svg';
import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
-import type { SvgProps } from 'react-native-svg';
-
-export type IconNode = [elementName: keyof ReactSVG, attrs: Record][];
-
-export interface LucideProps extends SvgProps {
- size?: string | number;
- absoluteStrokeWidth?: boolean;
- 'data-testid'?: string;
-}
-
-export type LucideIcon = ForwardRefExoticComponent;
+import { IconNode, LucideIcon, LucideProps } from './types';
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
const Component = forwardRef(
diff --git a/packages/lucide-react-native/src/lucide-react-native.ts b/packages/lucide-react-native/src/lucide-react-native.ts
index a024114981..6636c22592 100644
--- a/packages/lucide-react-native/src/lucide-react-native.ts
+++ b/packages/lucide-react-native/src/lucide-react-native.ts
@@ -1,9 +1,7 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
-export {
- default as createLucideIcon,
- type IconNode,
- type LucideProps,
- type LucideIcon,
-} from './createLucideIcon';
+export * from './types';
+
+export { default as createLucideIcon } from './createLucideIcon';
+export { default as Icon } from './Icon';
diff --git a/packages/lucide-react-native/src/types.ts b/packages/lucide-react-native/src/types.ts
new file mode 100644
index 0000000000..fd534414ac
--- /dev/null
+++ b/packages/lucide-react-native/src/types.ts
@@ -0,0 +1,12 @@
+import type { ForwardRefExoticComponent, ReactSVG } from 'react';
+import type { SvgProps } from 'react-native-svg';
+
+export type IconNode = [elementName: keyof ReactSVG, attrs: Record][];
+
+export interface LucideProps extends SvgProps {
+ size?: string | number;
+ absoluteStrokeWidth?: boolean;
+ 'data-testid'?: string;
+}
+
+export type LucideIcon = ForwardRefExoticComponent;
diff --git a/packages/lucide-react-native/tests/Icon.spec.tsx b/packages/lucide-react-native/tests/Icon.spec.tsx
new file mode 100644
index 0000000000..0bf4e886ad
--- /dev/null
+++ b/packages/lucide-react-native/tests/Icon.spec.tsx
@@ -0,0 +1,35 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render } from '@testing-library/react';
+
+import { airVent } from './testIconNodes';
+import { Icon } from '../src/lucide-react-native';
+
+vi.mock('react-native-svg');
+
+describe('Using Icon Component', () => {
+ it('should render icon based on a iconNode', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-react-native/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-react-native/tests/__snapshots__/Icon.spec.tsx.snap
new file mode 100644
index 0000000000..fd704741d4
--- /dev/null
+++ b/packages/lucide-react-native/tests/__snapshots__/Icon.spec.tsx.snap
@@ -0,0 +1,48 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using Icon Component > should render icon and match snapshot 1`] = `
+
+`;
diff --git a/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap b/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap
index 90dca01ac4..14af0d45ef 100644
--- a/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap
+++ b/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap
@@ -10,7 +10,6 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
- data-testid="grid-icon"
>
should not scale the strokeWidth when ab
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
- data-testid="grid-icon"
>
{
});
it('should adjust the size, stroke color and stroke width', () => {
- const testId = 'grid-icon';
- const { container, getByTestId } = render(
+ const { container } = render(
,
);
- const { attributes } = getByTestId(testId);
- expect((attributes as unknown as Attributes).stroke.value).toBe('red');
- expect((attributes as unknown as Attributes).width.value).toBe('48');
- expect((attributes as unknown as Attributes).height.value).toBe('48');
- expect((attributes as unknown as Attributes)['stroke-width'].value).toBe('4');
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '4');
expect(container.innerHTML).toMatchSnapshot();
});
@@ -61,23 +60,20 @@ describe('Using lucide icon components', () => {
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
- const testId = 'grid-icon';
- const { container, getByTestId } = render(
+ const { container } = render(
,
);
- const { attributes } = getByTestId(testId) as unknown as {
- attributes: Record;
- };
- expect(attributes.stroke.value).toBe('red');
- expect(attributes.width.value).toBe('48');
- expect(attributes.height.value).toBe('48');
- expect(attributes['stroke-width'].value).toBe('1');
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '1');
expect(container.innerHTML).toMatchSnapshot();
});
@@ -91,8 +87,8 @@ describe('Using lucide icon components', () => {
,
);
- const { children } = getByTestId(testId) as unknown as { children: HTMLCollection };
- const lastChild = children[children.length - 1];
+ const { children } = container.firstElementChild ?? {};
+ const lastChild = children?.[children.length - 1];
expect(lastChild).toEqual(getByTestId(childId));
expect(container.innerHTML).toMatchSnapshot();
diff --git a/packages/lucide-react-native/tests/testIconNodes.ts b/packages/lucide-react-native/tests/testIconNodes.ts
new file mode 100644
index 0000000000..c93721961c
--- /dev/null
+++ b/packages/lucide-react-native/tests/testIconNodes.ts
@@ -0,0 +1,22 @@
+import { IconNode } from '../src/types';
+
+export const airVent: IconNode = [
+ [
+ 'path',
+ {
+ d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
+ key: 'larmp2',
+ },
+ ],
+ ['path', { d: 'M6 8h12', key: '6g4wlu' }],
+ ['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }],
+ ['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }],
+];
+
+export const coffee: IconNode = [
+ ['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }],
+ ['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }],
+ ['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }],
+ ['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }],
+ ['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }],
+];
diff --git a/packages/lucide-react/src/Icon.ts b/packages/lucide-react/src/Icon.ts
new file mode 100644
index 0000000000..c623c0e3e5
--- /dev/null
+++ b/packages/lucide-react/src/Icon.ts
@@ -0,0 +1,59 @@
+import { createElement, forwardRef } from 'react';
+import defaultAttributes from './defaultAttributes';
+import { IconNode, LucideProps } from './types';
+import { mergeClasses } from '@lucide/shared';
+
+interface IconComponentProps extends LucideProps {
+ iconNode: IconNode;
+}
+
+/**
+ * Lucide icon component
+ *
+ * @component Icon
+ * @param {object} props
+ * @param {string} props.color - The color of the icon
+ * @param {number} props.size - The size of the icon
+ * @param {number} props.strokeWidth - The stroke width of the icon
+ * @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
+ * @param {string} props.className - The class name of the icon
+ * @param {IconNode} props.children - The children of the icon
+ * @param {IconNode} props.iconNode - The icon node of the icon
+ *
+ * @returns {ForwardRefExoticComponent} LucideIcon
+ */
+const Icon = forwardRef(
+ (
+ {
+ color = 'currentColor',
+ size = 24,
+ strokeWidth = 2,
+ absoluteStrokeWidth,
+ className = '',
+ children,
+ iconNode,
+ ...rest
+ },
+ ref,
+ ) => {
+ return createElement(
+ 'svg',
+ {
+ ref,
+ ...defaultAttributes,
+ width: size,
+ height: size,
+ stroke: color,
+ strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
+ className: mergeClasses('lucide', className),
+ ...rest,
+ },
+ [
+ ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
+ ...(Array.isArray(children) ? children : [children]),
+ ],
+ );
+ },
+);
+
+export default Icon;
diff --git a/packages/lucide-react/src/createLucideIcon.ts b/packages/lucide-react/src/createLucideIcon.ts
index 5207168afc..b209293824 100644
--- a/packages/lucide-react/src/createLucideIcon.ts
+++ b/packages/lucide-react/src/createLucideIcon.ts
@@ -1,60 +1,22 @@
-import {
- forwardRef,
- createElement,
- ReactSVG,
- SVGProps,
- ForwardRefExoticComponent,
- RefAttributes,
-} from 'react';
-import defaultAttributes from './defaultAttributes';
-import { toKebabCase } from '@lucide/shared';
-
-export type IconNode = [elementName: keyof ReactSVG, attrs: Record][];
-
-export type SVGAttributes = Partial>;
-type ComponentAttributes = RefAttributes & SVGAttributes;
-
-export interface LucideProps extends ComponentAttributes {
- size?: string | number;
- absoluteStrokeWidth?: boolean;
-}
-
-export type LucideIcon = ForwardRefExoticComponent;
-
-const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
- const Component = forwardRef(
- (
- {
- color = 'currentColor',
- size = 24,
- strokeWidth = 2,
- absoluteStrokeWidth,
- className = '',
- children,
- ...rest
- },
+import { createElement, forwardRef } from 'react';
+import { mergeClasses, toKebabCase } from '@lucide/shared';
+import { IconNode, LucideProps } from './types';
+import Icon from './Icon';
+
+/**
+ * Create a Lucide icon component
+ * @param {string} iconName
+ * @param {array} iconNode
+ * @returns {ForwardRefExoticComponent} LucideIcon
+ */
+const createLucideIcon = (iconName: string, iconNode: IconNode) => {
+ const Component = forwardRef(({ className, ...props }, ref) =>
+ createElement(Icon, {
ref,
- ) => {
- return createElement(
- 'svg',
- {
- ref,
- ...defaultAttributes,
- width: size,
- height: size,
- stroke: color,
- strokeWidth: absoluteStrokeWidth
- ? (Number(strokeWidth) * 24) / Number(size)
- : strokeWidth,
- className: ['lucide', `lucide-${toKebabCase(iconName)}`, className].join(' '),
- ...rest,
- },
- [
- ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
- ...(Array.isArray(children) ? children : [children]),
- ],
- );
- },
+ iconNode,
+ className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
+ ...props,
+ }),
);
Component.displayName = `${iconName}`;
diff --git a/packages/lucide-react/src/lucide-react.ts b/packages/lucide-react/src/lucide-react.ts
index a024114981..6636c22592 100644
--- a/packages/lucide-react/src/lucide-react.ts
+++ b/packages/lucide-react/src/lucide-react.ts
@@ -1,9 +1,7 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
-export {
- default as createLucideIcon,
- type IconNode,
- type LucideProps,
- type LucideIcon,
-} from './createLucideIcon';
+export * from './types';
+
+export { default as createLucideIcon } from './createLucideIcon';
+export { default as Icon } from './Icon';
diff --git a/packages/lucide-react/src/types.ts b/packages/lucide-react/src/types.ts
new file mode 100644
index 0000000000..ed686b315b
--- /dev/null
+++ b/packages/lucide-react/src/types.ts
@@ -0,0 +1,15 @@
+import { ReactSVG, SVGProps, ForwardRefExoticComponent, RefAttributes } from 'react';
+
+export type IconNode = [elementName: keyof ReactSVG, attrs: Record][];
+
+export type SVGAttributes = Partial>;
+type ElementAttributes = RefAttributes & SVGAttributes;
+
+export interface LucideProps extends ElementAttributes {
+ size?: string | number;
+ absoluteStrokeWidth?: boolean;
+}
+
+export type LucideIcon = ForwardRefExoticComponent<
+ Omit & RefAttributes
+>;
diff --git a/packages/lucide-react/tests/Icon.spec.tsx b/packages/lucide-react/tests/Icon.spec.tsx
new file mode 100644
index 0000000000..906f0c3117
--- /dev/null
+++ b/packages/lucide-react/tests/Icon.spec.tsx
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/react';
+
+import { airVent } from './testIconNodes';
+import { Icon } from '../src/lucide-react';
+
+describe('Using Icon Component', () => {
+ it('should render icon based on a iconNode', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap
new file mode 100644
index 0000000000..635ca428ad
--- /dev/null
+++ b/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap
@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using Icon Component > should render icon and match snapshot 1`] = `
+
+`;
diff --git a/packages/lucide-react/tests/__snapshots__/createLucideIcon.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/createLucideIcon.spec.tsx.snap
new file mode 100644
index 0000000000..acf4f97390
--- /dev/null
+++ b/packages/lucide-react/tests/__snapshots__/createLucideIcon.spec.tsx.snap
@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using createLucideIcon > should create a component from an iconNode 1`] = `
+
+`;
diff --git a/packages/lucide-react/tests/__snapshots__/dynamicImports.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/dynamicImports.spec.tsx.snap
new file mode 100644
index 0000000000..81acb42979
--- /dev/null
+++ b/packages/lucide-react/tests/__snapshots__/dynamicImports.spec.tsx.snap
@@ -0,0 +1,36 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using dynamicImports > should render icons dynamically by using the dynamicIconImports module 1`] = `
+
+`;
diff --git a/packages/lucide-react/tests/__snapshots__/lucide-react.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/lucide-react.spec.tsx.snap
index 31ab33829f..a3cc086577 100644
--- a/packages/lucide-react/tests/__snapshots__/lucide-react.spec.tsx.snap
+++ b/packages/lucide-react/tests/__snapshots__/lucide-react.spec.tsx.snap
@@ -10,8 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
- data-testid="grid-icon"
+ class="lucide lucide-grid3x3"
>
should not scale the strokeWidth when ab
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
- data-testid="grid-icon"
+ class="lucide lucide-grid3x3"
>
should render an component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-grid3x3 "
+ class="lucide lucide-grid3x3"
>
should render an component 1`] = `
`;
-
-exports[`Using lucide icon components > should render icons dynamically by using the dynamicIconImports module 1`] = `
-
-`;
diff --git a/packages/lucide-react/tests/createLucideIcon.spec.tsx b/packages/lucide-react/tests/createLucideIcon.spec.tsx
new file mode 100644
index 0000000000..7c1b5e475b
--- /dev/null
+++ b/packages/lucide-react/tests/createLucideIcon.spec.tsx
@@ -0,0 +1,15 @@
+import { describe, it, expect } from 'vitest';
+import { createLucideIcon } from '../src/lucide-react';
+import { airVent } from './testIconNodes';
+import { render } from '@testing-library/react';
+
+describe('Using createLucideIcon', () => {
+ it('should create a component from an iconNode', () => {
+ const AirVent = createLucideIcon('AirVent', airVent);
+
+ const { container } = render();
+
+ expect(container.firstChild).toMatchSnapshot();
+ expect(container.firstChild).toBeDefined();
+ });
+});
diff --git a/packages/lucide-react/tests/dynamicImports.spec.tsx b/packages/lucide-react/tests/dynamicImports.spec.tsx
new file mode 100644
index 0000000000..00f1eab8e4
--- /dev/null
+++ b/packages/lucide-react/tests/dynamicImports.spec.tsx
@@ -0,0 +1,38 @@
+import { describe, it, expect } from 'vitest';
+import { Suspense, lazy } from 'react';
+import { render, waitFor } from '@testing-library/react';
+
+import dynamicIconImports from '../src/dynamicIconImports';
+import { LucideProps } from '../src/types';
+
+describe('Using dynamicImports', () => {
+ it('should render icons dynamically by using the dynamicIconImports module', async () => {
+ interface IconProps extends Omit {
+ name: keyof typeof dynamicIconImports;
+ }
+
+ const Icon = ({ name, ...props }: IconProps) => {
+ const LucideIcon = lazy(dynamicIconImports[name]);
+
+ return (
+
+
+
+ );
+ };
+
+ const { container, getByLabelText } = render(
+ ,
+ );
+
+ await waitFor(() => getByLabelText('smile'));
+
+ expect(container.innerHTML).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-react/tests/lucide-react.spec.tsx b/packages/lucide-react/tests/lucide-react.spec.tsx
index 5d282290a9..8365d96f4a 100644
--- a/packages/lucide-react/tests/lucide-react.spec.tsx
+++ b/packages/lucide-react/tests/lucide-react.spec.tsx
@@ -1,8 +1,7 @@
import { describe, it, expect } from 'vitest';
-import { render, cleanup, waitFor } from '@testing-library/react';
-import { Pen, Edit2, Grid, LucideProps, Droplet } from '../src/lucide-react';
-import { Suspense, lazy } from 'react';
-import dynamicIconImports from '../src/dynamicIconImports';
+import { render, cleanup } from '@testing-library/react';
+import { Pen, Edit2, Grid, Droplet } from '../src/lucide-react';
+import defaultAttributes from '../src/defaultAttributes';
describe('Using lucide icon components', () => {
it('should render an component', () => {
@@ -11,24 +10,37 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
+ it('should render the icon with default attributes', () => {
+ const { container } = render();
+
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('xmlns', defaultAttributes.xmlns);
+ expect(SVGElement).toHaveAttribute('width', String(defaultAttributes.width));
+ expect(SVGElement).toHaveAttribute('height', String(defaultAttributes.height));
+ expect(SVGElement).toHaveAttribute('viewBox', defaultAttributes.viewBox);
+ expect(SVGElement).toHaveAttribute('fill', defaultAttributes.fill);
+ expect(SVGElement).toHaveAttribute('stroke', defaultAttributes.stroke);
+ expect(SVGElement).toHaveAttribute('stroke-width', String(defaultAttributes.strokeWidth));
+ expect(SVGElement).toHaveAttribute('stroke-linecap', defaultAttributes.strokeLinecap);
+ expect(SVGElement).toHaveAttribute('stroke-linejoin', defaultAttributes.strokeLinejoin);
+ });
+
it('should adjust the size, stroke color and stroke width', () => {
- const testId = 'grid-icon';
- const { container, getByTestId } = render(
+ const { container } = render(
,
);
- const { attributes } = getByTestId(testId) as unknown as {
- attributes: Record;
- };
- expect(attributes.stroke.value).toBe('red');
- expect(attributes.width.value).toBe('48');
- expect(attributes.height.value).toBe('48');
- expect(attributes['stroke-width'].value).toBe('4');
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '4');
expect(container.innerHTML).toMatchSnapshot();
});
@@ -58,23 +70,20 @@ describe('Using lucide icon components', () => {
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
- const testId = 'grid-icon';
const { container, getByTestId } = render(
,
);
- const { attributes } = getByTestId(testId) as unknown as {
- attributes: Record;
- };
- expect(attributes.stroke.value).toBe('red');
- expect(attributes.width.value).toBe('48');
- expect(attributes.height.value).toBe('48');
- expect(attributes['stroke-width'].value).toBe('1');
+ const SVGElement = container.firstElementChild;
+
+ expect(SVGElement).toHaveAttribute('stroke', 'red');
+ expect(SVGElement).toHaveAttribute('width', '48');
+ expect(SVGElement).toHaveAttribute('height', '48');
+ expect(SVGElement).toHaveAttribute('stroke-width', '1');
expect(container.innerHTML).toMatchSnapshot();
});
@@ -87,34 +96,4 @@ describe('Using lucide icon components', () => {
expect(container.firstChild).toHaveClass('lucide');
expect(container.firstChild).toHaveClass('lucide-droplet');
});
-
- it('should render icons dynamically by using the dynamicIconImports module', async () => {
- interface IconProps extends Omit {
- name: keyof typeof dynamicIconImports;
- }
-
- const Icon = ({ name, ...props }: IconProps) => {
- const LucideIcon = lazy(dynamicIconImports[name]);
-
- return (
-
-
-
- );
- };
-
- const { container, getByLabelText } = render(
- ,
- );
-
- await waitFor(() => getByLabelText('smile'));
-
- expect(container.innerHTML).toMatchSnapshot();
- });
});
diff --git a/packages/lucide-react/tests/testIconNodes.ts b/packages/lucide-react/tests/testIconNodes.ts
new file mode 100644
index 0000000000..c93721961c
--- /dev/null
+++ b/packages/lucide-react/tests/testIconNodes.ts
@@ -0,0 +1,22 @@
+import { IconNode } from '../src/types';
+
+export const airVent: IconNode = [
+ [
+ 'path',
+ {
+ d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
+ key: 'larmp2',
+ },
+ ],
+ ['path', { d: 'M6 8h12', key: '6g4wlu' }],
+ ['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }],
+ ['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }],
+];
+
+export const coffee: IconNode = [
+ ['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }],
+ ['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }],
+ ['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }],
+ ['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }],
+ ['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }],
+];
diff --git a/packages/lucide-solid/src/Icon.tsx b/packages/lucide-solid/src/Icon.tsx
index 5e92bd4a16..d805f72ca4 100644
--- a/packages/lucide-solid/src/Icon.tsx
+++ b/packages/lucide-solid/src/Icon.tsx
@@ -2,10 +2,10 @@ import { For, splitProps } from 'solid-js';
import { Dynamic } from 'solid-js/web';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
-import { toKebabCase } from '@lucide/shared';
+import { mergeClasses, toKebabCase } from '@lucide/shared';
interface IconProps {
- name: string;
+ name?: string;
iconNode: IconNode;
}
@@ -33,9 +33,12 @@ const Icon = (props: LucideProps & IconProps) => {
Number(localProps.size)
: Number(localProps.strokeWidth ?? defaultAttributes['stroke-width'])
}
- class={`lucide lucide-${toKebabCase(localProps?.name ?? 'icon')} ${
- localProps.class != null ? localProps.class : ''
- }`}
+ class={mergeClasses(
+ 'lucide',
+ 'lucide-icon',
+ localProps.name != null ? `lucide-${toKebabCase(localProps?.name)}` : undefined,
+ localProps.class != null ? localProps.class : '',
+ )}
{...rest}
>
diff --git a/packages/lucide-solid/src/lucide-solid.ts b/packages/lucide-solid/src/lucide-solid.ts
index 2e28382436..3bfeb70ea9 100644
--- a/packages/lucide-solid/src/lucide-solid.ts
+++ b/packages/lucide-solid/src/lucide-solid.ts
@@ -1,3 +1,6 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
+export * from './types';
+
+export { default as Icon } from './Icon';
diff --git a/packages/lucide-solid/src/types.ts b/packages/lucide-solid/src/types.ts
index 80ad93fe76..cea277310d 100644
--- a/packages/lucide-solid/src/types.ts
+++ b/packages/lucide-solid/src/types.ts
@@ -11,3 +11,5 @@ export interface LucideProps extends SVGAttributes {
class?: string;
absoluteStrokeWidth?: boolean;
}
+
+export type LucideIcon = (props: LucideProps) => JSX.Element;
diff --git a/packages/lucide-solid/tests/Icon.spec.tsx b/packages/lucide-solid/tests/Icon.spec.tsx
new file mode 100644
index 0000000000..74c3936abb
--- /dev/null
+++ b/packages/lucide-solid/tests/Icon.spec.tsx
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { render } from '@solidjs/testing-library';
+
+import { airVent } from './testIconNodes';
+import { Icon } from '../src/lucide-solid';
+
+describe('Using Icon Component', () => {
+ it('should render icon based on a iconNode', async () => {
+ const { container } = render(() => (
+
+ ));
+
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render(() => (
+
+ ));
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-solid/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-solid/tests/__snapshots__/Icon.spec.tsx.snap
new file mode 100644
index 0000000000..3ec6f852d3
--- /dev/null
+++ b/packages/lucide-solid/tests/__snapshots__/Icon.spec.tsx.snap
@@ -0,0 +1,33 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using Icon Component > should render icon and match snapshot 1`] = `
+
+`;
diff --git a/packages/lucide-solid/tests/__snapshots__/lucide-solid.spec.tsx.snap b/packages/lucide-solid/tests/__snapshots__/lucide-solid.spec.tsx.snap
index 0b1d5289a7..47ba999bfa 100644
--- a/packages/lucide-solid/tests/__snapshots__/lucide-solid.spec.tsx.snap
+++ b/packages/lucide-solid/tests/__snapshots__/lucide-solid.spec.tsx.snap
@@ -10,7 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
height="48"
stroke="red"
stroke-width="4"
- class="lucide lucide-grid3x3 "
+ class="lucide lucide-icon lucide-grid3x3"
data-testid="grid-icon"
>
should not scale the strokeWidth when ab
height="48"
stroke="red"
stroke-width="1"
- class="lucide lucide-grid3x3 "
+ class="lucide lucide-icon lucide-grid3x3"
data-testid="grid-icon"
>
should render a component 1`] = `
height="24"
stroke="currentColor"
stroke-width="2"
- class="lucide lucide-grid3x3 "
+ class="lucide lucide-icon lucide-grid3x3"
>
+ import { mergeClasses } from '@lucide/shared'
import defaultAttributes from './defaultAttributes'
import type { IconNode } from './types';
- export let name: string
+ export let name: string | undefined = undefined
export let color = 'currentColor'
export let size: number | string = 24
export let strokeWidth: number | string = 2
@@ -21,7 +22,14 @@
? Number(strokeWidth) * 24 / Number(size)
: strokeWidth
}
- class={`lucide-icon lucide lucide-${name} ${$$props.class ?? ''}`}
+ class={
+ mergeClasses(
+ 'lucide-icon',
+ 'lucide',
+ name ? `lucide-${name}`: '',
+ $$props.class
+ )
+ }
>
{#each iconNode as [tag, attrs]}
diff --git a/packages/lucide-svelte/src/lucide-svelte.ts b/packages/lucide-svelte/src/lucide-svelte.ts
index 77e474d300..c1567a365b 100644
--- a/packages/lucide-svelte/src/lucide-svelte.ts
+++ b/packages/lucide-svelte/src/lucide-svelte.ts
@@ -3,3 +3,4 @@ export * as icons from './icons/index.js';
export * from './aliases.js';
export { default as defaultAttributes } from './defaultAttributes.js';
export * from './types.js';
+export { default as Icon } from './Icon.svelte';
diff --git a/packages/lucide-svelte/tests/Icon.spec.ts b/packages/lucide-svelte/tests/Icon.spec.ts
new file mode 100644
index 0000000000..e5f0eb3244
--- /dev/null
+++ b/packages/lucide-svelte/tests/Icon.spec.ts
@@ -0,0 +1,33 @@
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/svelte';
+import { Icon } from '../src/lucide-svelte';
+
+import { airVent } from './testIconNodes';
+
+describe('Using Icon Component', () => {
+ it('should render icon based on a iconNode', async () => {
+ const { container } = render(Icon, {
+ props: {
+ iconNode: airVent,
+ size: 48,
+ color: 'red',
+ absoluteStrokeWidth: true,
+ },
+ });
+
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render(Icon, {
+ props: {
+ iconNode: airVent,
+ size: 48,
+ color: 'red',
+ absoluteStrokeWidth: true,
+ },
+ });
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-svelte/tests/__snapshots__/Icon.spec.ts.snap b/packages/lucide-svelte/tests/__snapshots__/Icon.spec.ts.snap
new file mode 100644
index 0000000000..bd3dddc7f1
--- /dev/null
+++ b/packages/lucide-svelte/tests/__snapshots__/Icon.spec.ts.snap
@@ -0,0 +1,36 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using Icon Component > should render icon and match snapshot 1`] = `
+
+`;
diff --git a/packages/lucide-svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap b/packages/lucide-svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap
index 5c44a60e5b..620513a6bc 100644
--- a/packages/lucide-svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap
+++ b/packages/lucide-svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap
@@ -45,7 +45,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and