diff --git a/src/__testing__/createIcon.test.tsx b/src/__testing__/createIcon.test.tsx
new file mode 100644
index 000000000..8cfbad91a
--- /dev/null
+++ b/src/__testing__/createIcon.test.tsx
@@ -0,0 +1,149 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+import { createIcon } from '../icons/createIcon';
+import { SistentThemeProviderWithoutBaseLine } from '../theme';
+
+function renderWithTheme(ui: React.ReactElement) {
+ return render(
+ {ui}
+ );
+}
+
+describe('createIcon', () => {
+ it('creates an icon with d string path definition', () => {
+ const TestIcon = createIcon({
+ displayName: 'TestPathIcon',
+ d: 'M10 10 H 90 V 90 H 10 Z'
+ });
+
+ expect(TestIcon.displayName).toBe('TestPathIcon');
+
+ const { container } = renderWithTheme();
+ const svg = container.querySelector('svg');
+ expect(svg).not.toBeNull();
+ expect(svg?.getAttribute('viewBox')).toBe('0 0 24 24');
+ expect(svg?.getAttribute('width')).toBe('24');
+ expect(svg?.getAttribute('height')).toBe('24');
+ expect(svg?.getAttribute('fill')).toBe('currentColor');
+
+ const path = container.querySelector('path');
+ expect(path).not.toBeNull();
+ expect(path?.getAttribute('d')).toBe('M10 10 H 90 V 90 H 10 Z');
+ expect(path?.getAttribute('fill')).toBe('currentColor');
+ });
+
+ it('creates an icon with path element (ReactNode)', () => {
+ const CustomIcon = createIcon({
+ displayName: 'CustomCircleIcon',
+ viewBox: '0 0 50 50',
+ path:
+ });
+
+ const { container } = renderWithTheme();
+ const svg = container.querySelector('svg');
+ expect(svg).not.toBeNull();
+ expect(svg?.getAttribute('viewBox')).toBe('0 0 50 50');
+
+ const circle = container.querySelector('circle');
+ expect(circle).not.toBeNull();
+ expect(circle?.getAttribute('cx')).toBe('25');
+ expect(circle?.getAttribute('cy')).toBe('25');
+ });
+
+ it('supports path as a function receiving props', () => {
+ const DynamicIcon = createIcon({
+ displayName: 'DynamicIcon',
+ path: (props) => (
+
+ )
+ });
+
+ const { container } = renderWithTheme();
+ const rect = container.querySelector('rect');
+ expect(rect).not.toBeNull();
+ expect(rect?.getAttribute('fill')).toBe('red');
+ });
+
+ it('forwards ref to the underlying svg element', () => {
+ const RefIcon = createIcon({
+ displayName: 'RefIcon',
+ d: 'M0 0h24v24H0z'
+ });
+
+ const ref = React.createRef();
+ renderWithTheme();
+
+ expect(ref.current).toBeInstanceOf(SVGSVGElement);
+ expect(ref.current?.tagName.toLowerCase()).toBe('svg');
+ });
+
+ it('allows overriding props (width, height, fill, className, style, data-testid)', () => {
+ const OverridableIcon = createIcon({
+ displayName: 'OverridableIcon',
+ d: 'M0 0h24v24H0z'
+ });
+
+ const { container } = renderWithTheme(
+
+ );
+
+ const svg = container.querySelector('svg');
+ expect(svg).not.toBeNull();
+ expect(svg?.getAttribute('width')).toBe('48');
+ expect(svg?.getAttribute('height')).toBe('3rem');
+ expect(svg?.getAttribute('fill')).toBe('#ff00ff');
+ expect(svg?.getAttribute('class')).toContain('custom-icon-class');
+ expect(svg?.getAttribute('data-testid')).toBe('custom-svg-id');
+ expect(svg?.style.opacity).toBe('0.8');
+
+ const path = container.querySelector('path');
+ expect(path).not.toBeNull();
+ expect(path?.getAttribute('fill')).toBe('#ff00ff');
+ });
+
+ it('applies defaultProps when provided', () => {
+ const DefaultedIcon = createIcon({
+ displayName: 'DefaultedIcon',
+ d: 'M0 0h24v24H0z',
+ defaultProps: {
+ fill: '#00B39F',
+ width: 32,
+ height: 32,
+ 'data-testid': 'default-test-id'
+ }
+ });
+
+ const { container } = renderWithTheme();
+ const svg = container.querySelector('svg');
+ expect(svg).not.toBeNull();
+ expect(svg?.getAttribute('width')).toBe('32');
+ expect(svg?.getAttribute('height')).toBe('32');
+ expect(svg?.getAttribute('fill')).toBe('#00B39F');
+ expect(svg?.getAttribute('data-testid')).toBe('default-test-id');
+ });
+
+ it('renders element when title prop is provided', () => {
+ const TitledIcon = createIcon({
+ displayName: 'TitledIcon',
+ d: 'M0 0h24v24H0z'
+ });
+
+ const { container } = renderWithTheme();
+ const title = container.querySelector('title');
+ expect(title).not.toBeNull();
+ expect(title?.textContent).toBe('Accessible Title');
+ });
+});
diff --git a/src/icons/AccessTime/AccessTimeIcon.tsx b/src/icons/AccessTime/AccessTimeIcon.tsx
index 82ce8860f..bbceb3a7d 100644
--- a/src/icons/AccessTime/AccessTimeIcon.tsx
+++ b/src/icons/AccessTime/AccessTimeIcon.tsx
@@ -1,26 +1,8 @@
-import { DEFAULT_FILL_NONE, DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants';
-import { IconProps } from '../types';
+import { createIcon } from '../createIcon';
-export const AccessTimeIcon = ({
- width = DEFAULT_WIDTH,
- height = DEFAULT_HEIGHT,
- fill = DEFAULT_FILL_NONE,
- ...props
-}: IconProps): JSX.Element => {
- return (
-
- );
-};
+export const AccessTimeIcon = createIcon({
+ displayName: 'AccessTimeIcon',
+ d: 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8m.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z'
+});
export default AccessTimeIcon;
diff --git a/src/icons/Add/AddIcon.tsx b/src/icons/Add/AddIcon.tsx
index 563551257..de85f276b 100644
--- a/src/icons/Add/AddIcon.tsx
+++ b/src/icons/Add/AddIcon.tsx
@@ -1,24 +1,11 @@
-import { DEFAULT_FILL_NONE, DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants';
-import { IconProps } from '../types';
+import { createIcon } from '../createIcon';
-export const AddIcon = ({
- width = DEFAULT_WIDTH,
- height = DEFAULT_HEIGHT,
- fill = DEFAULT_FILL_NONE,
- ...props
-}: IconProps): JSX.Element => {
- return (
-
- );
-};
+export const AddIcon = createIcon({
+ displayName: 'AddIcon',
+ d: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
+ defaultProps: {
+ 'data-testid': 'add-icon-svg'
+ }
+});
export default AddIcon;
diff --git a/src/icons/createIcon/createIcon.tsx b/src/icons/createIcon/createIcon.tsx
new file mode 100644
index 000000000..e1cc9b55b
--- /dev/null
+++ b/src/icons/createIcon/createIcon.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { DEFAULT_FILL_NONE, DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants';
+import { IconProps } from '../types';
+
+export interface CreateIconOptions {
+ /**
+ * The icon SVG viewBox.
+ * @default '0 0 24 24'
+ */
+ viewBox?: string;
+
+ /**
+ * The SVG path or elements to render inside the SVG.
+ */
+ path?: React.ReactNode | ((props: IconProps) => React.ReactNode);
+
+ /**
+ * Shorthand SVG path `d` attribute when providing a single path string.
+ */
+ d?: string;
+
+ /**
+ * The display name of the icon component (for React DevTools and testing).
+ */
+ displayName?: string;
+
+ /**
+ * Default props to override standard defaults (e.g. custom default fill or dimensions).
+ */
+ defaultProps?: Partial & Record;
+}
+
+export function createIcon(options: CreateIconOptions): React.ForwardRefExoticComponent<
+ IconProps & React.RefAttributes
+> {
+ const {
+ viewBox = '0 0 24 24',
+ d: pathDefinition,
+ path: pathElement,
+ displayName,
+ defaultProps = {}
+ } = options;
+
+ const Comp = React.forwardRef((props, ref) => {
+ const mergedProps = { ...defaultProps, ...props };
+ const {
+ width = DEFAULT_WIDTH,
+ height = DEFAULT_HEIGHT,
+ fill = DEFAULT_FILL_NONE,
+ title,
+ ...rest
+ } = mergedProps;
+
+ let content: React.ReactNode = null;
+ if (typeof pathElement === 'function') {
+ content = pathElement(mergedProps);
+ } else if (pathElement) {
+ content = pathElement;
+ } else if (pathDefinition) {
+ content = ;
+ }
+
+ return (
+
+ );
+ });
+
+ Comp.displayName = displayName || 'Icon';
+
+ return Comp;
+}
+
+export default createIcon;
diff --git a/src/icons/createIcon/index.ts b/src/icons/createIcon/index.ts
new file mode 100644
index 000000000..cdb278409
--- /dev/null
+++ b/src/icons/createIcon/index.ts
@@ -0,0 +1 @@
+export { createIcon, default, type CreateIconOptions } from './createIcon';
diff --git a/src/icons/index.ts b/src/icons/index.ts
index 8299cb93e..ee59fd13c 100644
--- a/src/icons/index.ts
+++ b/src/icons/index.ts
@@ -51,6 +51,7 @@ export * from './ContentClassIcons';
export * from './ContentFilter';
export * from './Copy';
export * from './CreateNew';
+export * from './createIcon';
export * from './Credential';
export * from './CrossCircle';
export * from './Dashboard';
diff --git a/src/index.tsx b/src/index.tsx
index 33a25a51f..22b5132df 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -164,3 +164,9 @@ export {
type Team as TeamPickerRecord,
type TeamSearchFieldProps
} from './custom/DashboardWidgets/GettingStartedWidget/TeamSearchField';
+
+export {
+ createIcon,
+ type CreateIconOptions
+} from './icons/createIcon';
+