+
+
{title}
+ {description ? (
+
{description}
+ ) : null}
+ {action || onRetry ? (
+
+ {action ?? (
+
+ )}
+
+ ) : null}
+
+ );
+}
diff --git a/packages/ui/src/components/Icon/Icon.css b/packages/ui/src/components/Icon/Icon.css
new file mode 100644
index 0000000000..3dcc21dde3
--- /dev/null
+++ b/packages/ui/src/components/Icon/Icon.css
@@ -0,0 +1,21 @@
+.ui-icon {
+ color: currentColor;
+ font-size: inherit;
+ line-height: 1;
+}
+
+.ui-icon--spin {
+ animation: ui-icon-spin 1s linear infinite;
+}
+
+@keyframes ui-icon-spin {
+ to {
+ transform: rotate(1turn);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .ui-icon--spin {
+ animation: none;
+ }
+}
diff --git a/packages/ui/src/components/Icon/Icon.stories.tsx b/packages/ui/src/components/Icon/Icon.stories.tsx
new file mode 100644
index 0000000000..9aeb9e4d42
--- /dev/null
+++ b/packages/ui/src/components/Icon/Icon.stories.tsx
@@ -0,0 +1,66 @@
+import { expect, within } from "storybook/test";
+
+import { FourThemeModes } from "#storybook";
+
+import { Icon } from "./Icon";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import type { CodiconName } from "#codicons";
+
+const galleryIcons = [
+ "account",
+ "add",
+ "alert",
+ "check",
+ "close",
+ "error",
+ "info",
+ "refresh",
+ "search",
+ "settings-gear",
+ "trash",
+ "workspace-trusted",
+] as const satisfies readonly CodiconName[];
+
+const IconStates = (): React.JSX.Element => (
+