diff --git a/packages/components/src/icon/icon.stories.ts b/packages/components/src/icon/icon.stories.ts
new file mode 100644
index 0000000..e7e7eb9
--- /dev/null
+++ b/packages/components/src/icon/icon.stories.ts
@@ -0,0 +1,43 @@
+import type { StoryFn, Meta, StoryObj } from '@storybook/html';
+import { Icon } from './index';
+
+// Register the icon with proper SVG formatting
+Icon.register({
+ name: 'search',
+ svgStr: `
+ `
+});
+
+export default {
+ title: 'Components/Icon',
+ argTypes: {
+ name: { control: 'select', options: ['search', 'default'] }
+ },
+ parameters: {
+ actions: { disabled: true }
+ }
+} as Meta;
+
+const Template: StoryFn = (args, context): string => {
+ if (args.delay !== undefined) {
+ setTimeout(() => {
+ Icon.register({
+ name: 'search',
+ svgStr: `
+ `
+ });
+ }, args.delay);
+ }
+
+ return ``;
+};
+
+export const Default: StoryObj = { render: Template.bind({}) };
+Default.args = { name: 'search' };
+export const ChangeIcon: StoryObj = { render: Template.bind({}) };
+ChangeIcon.args = {
+ ...Default.args,
+ delay: 2000 // Two seconds delay
+};
diff --git a/packages/components/src/icon/icon.test.ts b/packages/components/src/icon/icon.test.ts
new file mode 100644
index 0000000..ff6c481
--- /dev/null
+++ b/packages/components/src/icon/icon.test.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { test, expect } from '@playwright/test';
+
+test('Default', async ({ page }) => {
+ await page.goto('/iframe.html?id=components-icon--default');
+ expect(await page.locator('jp-icon').screenshot()).toMatchSnapshot(
+ 'icon-default.png'
+ );
+});
+test('ChangeIcon', async ({ page }) => {
+ await page.goto('/iframe.html?id=components-icon--change-icon');
+ await page.waitForTimeout(2000);
+ expect(await page.locator('jp-icon').screenshot()).toMatchSnapshot(
+ 'icon-change-icon.png'
+ );
+});
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-chromium-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-chromium-linux.png
new file mode 100644
index 0000000..8ed077c
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-chromium-linux.png differ
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-firefox-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-firefox-linux.png
new file mode 100644
index 0000000..05bfb87
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-firefox-linux.png differ
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-webkit-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-webkit-linux.png
new file mode 100644
index 0000000..3298f09
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-change-icon-webkit-linux.png differ
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-default-chromium-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-chromium-linux.png
new file mode 100644
index 0000000..662d255
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-chromium-linux.png differ
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-default-firefox-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-firefox-linux.png
new file mode 100644
index 0000000..49f57c6
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-firefox-linux.png differ
diff --git a/packages/components/src/icon/icon.test.ts-snapshots/icon-default-webkit-linux.png b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-webkit-linux.png
new file mode 100644
index 0000000..e464b65
Binary files /dev/null and b/packages/components/src/icon/icon.test.ts-snapshots/icon-default-webkit-linux.png differ
diff --git a/packages/components/src/icon/index.ts b/packages/components/src/icon/index.ts
new file mode 100644
index 0000000..41f75db
--- /dev/null
+++ b/packages/components/src/icon/index.ts
@@ -0,0 +1,85 @@
+import {
+ FASTElement,
+ customElement,
+ attr,
+ html
+} from '@microsoft/fast-element';
+
+const template = html``;
+
+/**
+ * Icon component
+ *
+ * Icon must first be registered: `Icon.register({ name, svgStr });`
+ *
+ * Then you can use it with `` .
+ *
+ * To style your icon, you should set `fill` and/or `stroke` attributes to `currentColor`.
+ * Then the icon will be colored with the active text color.
+ */
+@customElement({
+ name: 'jp-icon',
+ template
+})
+export class Icon extends FASTElement {
+ /**
+ * Name of the icon to display.
+ */
+ @attr name: string;
+
+ private static iconsMap = new Map();
+ private static _defaultIcon =
+ '';
+
+ /**
+ * Register a new icon.
+ *
+ * @param options { name: Icon unique name, svgStr: Icon SVG as string }
+ */
+ static register(options: { name: string; svgStr: string }): void {
+ if (Icon.iconsMap.has(options.name)) {
+ console.warn(
+ `Redefining previously loaded icon svgStr. name: ${
+ options.name
+ }, svgStrOld: ${Icon.iconsMap.get(options.name)}, svgStr: ${
+ options.svgStr
+ }`
+ );
+ }
+ Icon.iconsMap.set(options.name, options.svgStr);
+
+ // Rerender all existing icons with the same name
+ document
+ .querySelectorAll(`jp-icon[name="${options.name}"]`)
+ .forEach((node: HTMLElement) => {
+ node.setAttribute('name', '');
+ node.setAttribute('name', options.name);
+ });
+ }
+
+ /**
+ * Set a new default icon.
+ *
+ * @param svgStr The SVG string to be used as the default icon.
+ */
+ static setDefaultIcon(svgStr: string): void {
+ Icon._defaultIcon = svgStr;
+ }
+
+ /**
+ * Default icon
+ *
+ * This icon will be displayed if the {@link name} does not match any known
+ * icon names.
+ */
+ static defaultIcon(): string {
+ return Icon._defaultIcon;
+ }
+
+ /**
+ * Get the icon SVG
+ */
+ getSvg(): string {
+ return Icon.iconsMap.get(this.name) ?? Icon.defaultIcon();
+ }
+}