diff --git a/docs/guide/packages/lucide-react.md b/docs/guide/packages/lucide-react.md
index 4f5ca75b0e..45e7c10dee 100644
--- a/docs/guide/packages/lucide-react.md
+++ b/docs/guide/packages/lucide-react.md
@@ -85,108 +85,21 @@ const App = () => (
);
```
-## One generic icon component
+## Dynamic Icon Component
It is possible to create one generic icon component to load icons, but it is not recommended.
+Since it is importing all icons during build. This increases build time and the different modules it will create.
-::: danger
-The example below imports all ES Modules, so exercise caution when using it. Importing all icons will significantly increase the build size of the application, negatively affecting its performance. This is especially important to keep in mind when using bundlers like `Webpack`, `Rollup`, or `Vite`.
+`DynamicIcon` is useful for applications that want to show icons dynamically by icon name. For example, when using a content management system with where icon names are stored in a database.
-This is not the case for the latest NextJS, because it uses server side rendering. The icons will be streamed to the client when needed. For NextJS with Dynamic Imports, see [dynamic imports](#nextjs-example) section for more information.
-:::
+For static use cases, it is recommended to import the icons directly.
-### Icon Component Example
+The same props can be passed to adjust the icon appearance. The `name` prop is required to load the correct icon.
```jsx
-import { icons } from 'lucide-react';
-
-const Icon = ({ name, color, size }) => {
- const LucideIcon = icons[name];
-
- return ;
-};
-
-export default Icon;
-```
-
-#### Using the Icon Component
-
-```jsx
-import Icon from './Icon';
-
-const App = () => {
- return ;
-};
-
-export default App;
-```
-
-#### With Dynamic Imports
-
-Lucide react exports a dynamic import map `dynamicIconImports`, which is useful for applications that want to show icons dynamically by icon name. For example, when using a content management system with where icon names are stored in a database.
-
-When using client side rendering, it will fetch the icon component when it's needed. This will reduce the initial bundle size.
-
-The keys of the dynamic import map are the lucide original icon names (kebab case).
-
-Example with React suspense:
-
-```tsx
-import React, { lazy, Suspense } from 'react';
-import { LucideProps } from 'lucide-react';
-import dynamicIconImports from 'lucide-react/dynamicIconImports';
+import { DynamicIcon } from 'lucide-react/dynamic';
-const fallback =
-
-interface IconProps extends Omit {
- name: keyof typeof dynamicIconImports;
-}
-
-const Icon = ({ name, ...props }: IconProps) => {
- const LucideIcon = lazy(dynamicIconImports[name]);
-
- return (
-
-
-
- );
-}
-
-export default Icon
-```
-
-##### NextJS Example
-
-In NextJS, [the dynamic function](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) can be used to dynamically load the icon component.
-
-To make dynamic imports work with NextJS, you need to add `lucide-react` to the [`transpilePackages`](https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages) option in your `next.config.js` like this:
-
-```js
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- transpilePackages: ['lucide-react'] // add this
-}
-
-module.exports = nextConfig
-
-```
-
-You can then start using it:
-
-```tsx
-import dynamic from 'next/dynamic'
-import { LucideProps } from 'lucide-react';
-import dynamicIconImports from 'lucide-react/dynamicIconImports';
-
-interface IconProps extends LucideProps {
- name: keyof typeof dynamicIconImports;
-}
-
-const Icon = ({ name, ...props }: IconProps) => {
- const LucideIcon = dynamic(dynamicIconImports[name])
-
- return ;
-};
-
-export default Icon;
+const App = () => (
+
+);
```
diff --git a/packages/lucide-react/package.json b/packages/lucide-react/package.json
index 2c1ec40f09..5d98cddc24 100644
--- a/packages/lucide-react/package.json
+++ b/packages/lucide-react/package.json
@@ -23,18 +23,53 @@
],
"author": "Eric Fennis",
"amdName": "lucide-react",
+ "source": "src/lucide-react.ts",
"main": "dist/cjs/lucide-react.js",
- "main:umd": "dist/umd/lucide-react.js",
"module": "dist/esm/lucide-react.js",
- "unpkg": "dist/umd/lucide-react.min.js",
- "typings": "dist/lucide-react.d.ts",
- "sideEffects": false,
+ "types": "dist/lucide-react.d.ts",
"files": [
- "dist",
- "dynamicIconImports.js",
- "dynamicIconImports.js.map",
- "dynamicIconImports.d.ts"
+ "dist"
],
+ "exports": {
+ ".": {
+ "types": "./dist/lucide-react.d.ts",
+ "import": "./dist/esm/lucide-react.js",
+ "browser": "./dist/esm/lucide-react.js",
+ "require": "./dist/cjs/lucide-react.js",
+ "node": "./dist/cjs/lucide-react.js"
+ },
+ "./icons": {
+ "types": "./dist/lucide-react.d.ts",
+ "import": "./dist/esm/lucide-react.js",
+ "browser": "./dist/esm/lucide-react.js",
+ "require": "./dist/cjs/lucide-react.js",
+ "node": "./dist/cjs/lucide-react.js"
+ },
+ "./icons/*": {
+ "types": "./dist/icons/*.d.ts",
+ "import": "./dist/esm/icons/*.js",
+ "browser": "./dist/esm/icons/*.js",
+ "require": "./dist/cjs/icons/*.js",
+ "node": "./dist/cjs/icons/*.js"
+ },
+ "./dynamic": {
+ "types": "./dist/dynamic.d.ts",
+ "import": "./dist/esm/dynamic.js",
+ "browser": "./dist/esm/dynamic.js",
+ "require": "./dist/cjs/dynamic.js",
+ "node": "./dist/cjs/dynamic.js"
+ },
+ "./dynamicIconImports": {
+ "types": "./dist/dynamicIconImports.d.ts",
+ "import": "./dist/esm/dynamicIconImports.js",
+ "browser": "./dist/esm/dynamicIconImports.js",
+ "require": "./dist/cjs/dynamicIconImports.js",
+ "node": "./dist/cjs/dynamicIconImports.js"
+ },
+ "./src/*": "./src/*.ts",
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm typecheck && pnpm build:bundles",
"copy:license": "cp ../../LICENSE ./LICENSE",
@@ -60,6 +95,7 @@
"react-dom": "18.2.0",
"rollup": "^4.22.4",
"rollup-plugin-dts": "^6.1.0",
+ "rollup-plugin-preserve-directives": "^0.4.0",
"typescript": "^4.9.5",
"vite": "5.1.8",
"vitest": "^1.1.1"
diff --git a/packages/lucide-react/rollup.config.mjs b/packages/lucide-react/rollup.config.mjs
index 5f76f7e113..a980b4fdef 100644
--- a/packages/lucide-react/rollup.config.mjs
+++ b/packages/lucide-react/rollup.config.mjs
@@ -1,4 +1,5 @@
import plugins from '@lucide/rollup-plugins';
+import preserveDirectives from 'rollup-plugin-preserve-directives';
import pkg from './package.json' assert { type: 'json' };
import dts from 'rollup-plugin-dts';
import getAliasesEntryNames from './scripts/getAliasesEntryNames.mjs';
@@ -34,14 +35,15 @@ const bundles = [
},
{
format: 'esm',
- inputs: ['src/dynamicIconImports.ts'],
- outputFile: 'dynamicIconImports.js',
+ inputs: ['src/dynamic.ts', 'src/dynamicIconImports.ts', 'src/DynamicIcon.ts'],
+ outputDir,
+ preserveModules: true,
external: [/src/],
paths: (id) => {
if (id.match(/src/)) {
const [, modulePath] = id.match(/src\/(.*)\.ts/);
- return `dist/esm/${modulePath}.js`;
+ return `${modulePath}.js`;
}
},
},
@@ -62,7 +64,14 @@ const configs = bundles
}) =>
inputs.map((input) => ({
input,
- plugins: plugins({ pkg, minify }),
+ plugins: [
+ ...plugins({ pkg, minify }),
+ // Make sure we emit "use client" directive to make it compatible with Next.js
+ preserveDirectives({
+ include: 'src/DynamicIcon.ts',
+ suppressPreserveModulesWarning: true,
+ }),
+ ],
external: ['react', 'prop-types', ...external],
output: {
name: packageName,
@@ -95,7 +104,31 @@ export default [
input: 'src/dynamicIconImports.ts',
output: [
{
- file: `dynamicIconImports.d.ts`,
+ file: `dist/dynamicIconImports.d.ts`,
+ format: 'es',
+ },
+ ],
+ plugins: [
+ dts({
+ exclude: ['./src/icons'],
+ }),
+ ],
+ },
+ {
+ input: 'src/dynamic.ts',
+ output: [
+ {
+ file: `dist/dynamic.d.ts`,
+ format: 'es',
+ },
+ ],
+ plugins: [dts()],
+ },
+ {
+ input: 'src/DynamicIcon.ts',
+ output: [
+ {
+ file: `dist/DynamicIcon.d.ts`,
format: 'es',
},
],
diff --git a/packages/lucide-react/scripts/exportTemplate.mjs b/packages/lucide-react/scripts/exportTemplate.mjs
index e6fd638fa8..0a26577df1 100644
--- a/packages/lucide-react/scripts/exportTemplate.mjs
+++ b/packages/lucide-react/scripts/exportTemplate.mjs
@@ -7,6 +7,9 @@ export default ({ componentName, iconName, children, getSvg, deprecated, depreca
return `
import createLucideIcon from '../createLucideIcon';
+import { IconNode } from '../types';
+
+export const __iconNode: IconNode = ${JSON.stringify(children)}
/**
* @component @name ${componentName}
@@ -19,7 +22,7 @@ import createLucideIcon from '../createLucideIcon';
* @returns {JSX.Element} JSX Element
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
-const ${componentName} = createLucideIcon('${componentName}', ${JSON.stringify(children)});
+const ${componentName} = createLucideIcon('${componentName}', __iconNode);
export default ${componentName};
`;
diff --git a/packages/lucide-react/src/DynamicIcon.ts b/packages/lucide-react/src/DynamicIcon.ts
new file mode 100644
index 0000000000..773bab4f9d
--- /dev/null
+++ b/packages/lucide-react/src/DynamicIcon.ts
@@ -0,0 +1,73 @@
+'use client';
+
+import { createElement, forwardRef, useEffect, useState } from 'react';
+import { IconNode, LucideIcon, LucideProps } from './types';
+import dynamicIconImports from './dynamicIconImports';
+import Icon from './Icon';
+
+export type DynamicIconModule = { default: LucideIcon; __iconNode: IconNode };
+
+export type IconName = keyof typeof dynamicIconImports;
+
+export const iconNames = Object.keys(dynamicIconImports) as Array;
+
+interface DynamicIconComponentProps extends LucideProps {
+ name: IconName;
+ fallback?: () => JSX.Element | null;
+}
+
+async function getIconNode(name: IconName) {
+ if (!(name in dynamicIconImports)) {
+ throw new Error('[lucide-react]: Name in Lucide DynamicIcon not found');
+ }
+
+ // TODO: Replace this with a generic iconNode package.
+ const icon = (await dynamicIconImports[name]()) as DynamicIconModule;
+
+ return icon.__iconNode;
+}
+
+/**
+ * Dynamic 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 DynamicIcon = forwardRef(
+ ({ name, fallback: Fallback, ...props }, ref) => {
+ const [iconNode, setIconNode] = useState();
+
+ useEffect(() => {
+ getIconNode(name)
+ .then(setIconNode)
+ .catch((error) => {
+ console.error(error);
+ });
+ }, [name]);
+
+ if (iconNode == null) {
+ if (Fallback == null) {
+ return null;
+ }
+
+ return createElement(Fallback);
+ }
+
+ return createElement(Icon, {
+ ref,
+ ...props,
+ iconNode,
+ });
+ },
+);
+
+export default DynamicIcon;
diff --git a/packages/lucide-react/src/dynamic.ts b/packages/lucide-react/src/dynamic.ts
new file mode 100644
index 0000000000..f4bdb822a2
--- /dev/null
+++ b/packages/lucide-react/src/dynamic.ts
@@ -0,0 +1,7 @@
+export {
+ default as DynamicIcon,
+ iconNames,
+ type DynamicIconModule,
+ type IconName,
+} from './DynamicIcon';
+export { default as dynamicIconImports } from './dynamicIconImports';
diff --git a/packages/lucide-react/tests/DynamicIcon.spec.tsx b/packages/lucide-react/tests/DynamicIcon.spec.tsx
new file mode 100644
index 0000000000..b3e1321f47
--- /dev/null
+++ b/packages/lucide-react/tests/DynamicIcon.spec.tsx
@@ -0,0 +1,55 @@
+import { describe, it, expect } from 'vitest';
+import { act, render, waitFor, type RenderResult } from '@testing-library/react';
+
+import DynamicIcon from '../src/DynamicIcon';
+
+describe('Using DynamicIcon Component', () => {
+ it('should render icon by given name', async () => {
+ let container: RenderResult['container'];
+
+ await act(async () => {
+ const result = render();
+
+ container = result.container;
+ });
+
+ await waitFor(() => {
+ // I'd look for a real text here that is renderer when the data loads
+ expect(container.firstChild).not.toBeNull();
+ });
+ });
+
+ it('should render icon by alias name', async () => {
+ let container: RenderResult['container'];
+
+ await act(async () => {
+ const result = render();
+
+ container = result.container;
+ });
+
+ await waitFor(() => {
+ // I'd look for a real text here that is renderer when the data loads
+ expect(container.firstChild).not.toBeNull();
+ });
+ });
+
+ it('should render icon and match snapshot', async () => {
+ const { container } = render();
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('should adjust the style based', async () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/packages/lucide-react/tests/__snapshots__/DynamicIcon.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/DynamicIcon.spec.tsx.snap
new file mode 100644
index 0000000000..2c85a3f6a6
--- /dev/null
+++ b/packages/lucide-react/tests/__snapshots__/DynamicIcon.spec.tsx.snap
@@ -0,0 +1,5 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Using DynamicIcon Component > should adjust the style based 1`] = `null`;
+
+exports[`Using DynamicIcon Component > should render icon and match snapshot 1`] = `null`;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 347e8c5ed0..ff6b3db4d3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -402,6 +402,9 @@ importers:
rollup-plugin-dts:
specifier: ^6.1.0
version: 6.1.0(rollup@4.22.4)(typescript@4.9.5)
+ rollup-plugin-preserve-directives:
+ specifier: ^0.4.0
+ version: 0.4.0(rollup@4.22.4)
typescript:
specifier: ^4.9.5
version: 4.9.5
@@ -10022,6 +10025,11 @@ packages:
peerDependencies:
rollup: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0
+ rollup-plugin-preserve-directives@0.4.0:
+ resolution: {integrity: sha512-gx4nBxYm5BysmEQS+e2tAMrtFxrGvk+Pe5ppafRibQi0zlW7VYAbEGk6IKDw9sJGPdFWgVTE0o4BU4cdG0Fylg==}
+ peerDependencies:
+ rollup: 2.x || 3.x || 4.x
+
rollup-plugin-sourcemaps@0.6.3:
resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==}
engines: {node: '>=10.0.0'}
@@ -10611,6 +10619,7 @@ packages:
sudo-prompt@9.2.1:
resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==}
+ deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
superjson@2.2.1:
resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==}
@@ -17582,6 +17591,18 @@ snapshots:
postcss: 8.4.49
source-map: 0.6.1
+ '@vue/compiler-sfc@3.4.18':
+ dependencies:
+ '@babel/parser': 7.23.9
+ '@vue/compiler-core': 3.4.18
+ '@vue/compiler-dom': 3.4.18
+ '@vue/compiler-ssr': 3.4.18
+ '@vue/shared': 3.4.18
+ estree-walker: 2.0.2
+ magic-string: 0.30.11
+ postcss: 8.4.41
+ source-map-js: 1.2.1
+
'@vue/compiler-sfc@3.4.21':
dependencies:
'@babel/parser': 7.26.1
@@ -24141,6 +24162,12 @@ snapshots:
spdx-expression-validate: 2.0.0
spdx-satisfies: 5.0.1
+ rollup-plugin-preserve-directives@0.4.0(rollup@4.22.4):
+ dependencies:
+ '@rollup/pluginutils': 5.1.0(rollup@4.22.4)
+ magic-string: 0.30.11
+ rollup: 4.22.4
+
rollup-plugin-sourcemaps@0.6.3(@types/node@12.20.55)(rollup@2.79.1):
dependencies:
'@rollup/pluginutils': 3.1.0(rollup@2.79.1)
diff --git a/tools/build-icons/building/generateDynamicImports.mjs b/tools/build-icons/building/generateDynamicImports.mjs
index 4a2e046938..2a710c1d61 100644
--- a/tools/build-icons/building/generateDynamicImports.mjs
+++ b/tools/build-icons/building/generateDynamicImports.mjs
@@ -5,6 +5,7 @@ export default function generateDynamicImports({
iconNodes,
outputDirectory,
fileExtension,
+ iconMetaData,
showLog = true,
}) {
const fileName = path.basename(`dynamicIconImports${fileExtension}`);
@@ -18,6 +19,23 @@ export default function generateDynamicImports({
// Generate Import for Icon VNodes
icons.forEach((iconName) => {
importString += ` '${iconName}': () => import('./icons/${iconName}'),\n`;
+
+ const iconAliases = iconMetaData[iconName]?.aliases?.map((alias) => {
+ if (typeof alias === 'string') {
+ return alias;
+ }
+ return alias.name;
+ });
+
+ if (iconAliases != null && Array.isArray(iconAliases)) {
+ iconAliases.forEach((alias) => {
+ if (!alias) {
+ return;
+ }
+
+ importString += ` '${alias}': () => import('./icons/${iconName}'),\n`;
+ });
+ }
});
importString += '};\nexport default dynamicIconImports;\n';
diff --git a/tools/build-icons/cli.mjs b/tools/build-icons/cli.mjs
index c710cf7076..934bb0b873 100755
--- a/tools/build-icons/cli.mjs
+++ b/tools/build-icons/cli.mjs
@@ -90,6 +90,7 @@ async function buildIcons() {
iconNodes: icons,
outputDirectory: OUTPUT_DIR,
fileExtension: aliasesFileExtension,
+ iconMetaData,
showLog: !silent,
});
}