diff --git a/.changeset/short-gifts-jam.md b/.changeset/short-gifts-jam.md
new file mode 100644
index 000000000000..f9a7b4a60dab
--- /dev/null
+++ b/.changeset/short-gifts-jam.md
@@ -0,0 +1,5 @@
+---
+"website": minor
+---
+
+feat:Custom component gallery
diff --git a/js/_website/src/lib/icons/Close.svelte b/js/_website/src/lib/icons/Close.svelte
new file mode 100644
index 000000000000..fc4a5580b62d
--- /dev/null
+++ b/js/_website/src/lib/icons/Close.svelte
@@ -0,0 +1,19 @@
+
diff --git a/js/_website/src/lib/icons/CopyButton.svelte b/js/_website/src/lib/icons/CopyButton.svelte
new file mode 100644
index 000000000000..c948a258d1ad
--- /dev/null
+++ b/js/_website/src/lib/icons/CopyButton.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/js/_website/src/routes/custom_components/gallery/+page.svelte b/js/_website/src/routes/custom_components/gallery/+page.svelte
new file mode 100644
index 000000000000..9fc17bccab65
--- /dev/null
+++ b/js/_website/src/routes/custom_components/gallery/+page.svelte
@@ -0,0 +1,160 @@
+
+
+
+
+
+ {#each components as component (component.id)}
+
handle_box_click(component)}
+ class="box h-36 group font:thin relative rounded-xl shadow-sm hover:shadow-alternate transition-shadow bg-gradient-to-r {component.background_color}"
+ >
+
+ {classToEmojiMapping[component.template] || "❓"}
+
+
+ {component.name}
+
+
+ Tags: {component.tags.split(",").join(", ")}
+
+ {/each}
+
+
+{#if selected_component}
+
+
+
+
+ {selected_component.name}
+
+
+
+
+{/if}
+
+
diff --git a/js/_website/src/routes/custom_components/gallery/Card.svelte b/js/_website/src/routes/custom_components/gallery/Card.svelte
new file mode 100644
index 000000000000..8eccca2f3f0d
--- /dev/null
+++ b/js/_website/src/routes/custom_components/gallery/Card.svelte
@@ -0,0 +1,145 @@
+
+
+
+
{data.description}
+
+
+
+ pip install {data.name.replace("_", "-")}
+
+
+
+
+
+ 📑 Template
+ {data.template}
+
+
+ 🔢 Version:
+ {data.version}
+
+
+
+
+
+ 🔖 Tags:
+ {data.tags.split(",").join(", ")}
+
+
+
+
+
+ {#each tabs as tab, index}
+
{
+ active_tab = index;
+ }}
+ >
+ {tab}
+
+ {/each}
+
+ {#if active_tab === 0}
+
+ {:else}
+
+
+
+ {/if}
+
+
+
+
diff --git a/js/_website/src/routes/custom_components/gallery/utils.ts b/js/_website/src/routes/custom_components/gallery/utils.ts
new file mode 100644
index 000000000000..88591a01213d
--- /dev/null
+++ b/js/_website/src/routes/custom_components/gallery/utils.ts
@@ -0,0 +1,43 @@
+export type ComponentData = {
+ id: string;
+ name: string;
+ template: string;
+ author: string;
+ description: string;
+ tags: string;
+ version: string;
+ subdomain: string;
+ background_color: string;
+};
+
+export function getRandomIntInclusive(min: number, max: number): number {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
+}
+
+export const classToEmojiMapping: { [key: string]: string } = {
+ AnnotatedImage: "🖼️",
+ Audio: "🔊",
+ Plot: "📈",
+ Button: "🔘",
+ Chatbot: "🤖",
+ Code: "💻",
+ ColorPicker: "🎨",
+ Dataframe: "📊",
+ Dataset: "📚",
+ Fallback: "🔄",
+ File: "📄",
+ FileExplorer: "📂",
+ Gallery: "🎨",
+ HighlightedText: "✨",
+ HTML: "🔗",
+ Image: "🖼️",
+ JSON: "📝",
+ Label: "🏷️",
+ Markdown: "📝",
+ Model3D: "🗿",
+ State: "🔢",
+ UploadButton: "📤",
+ Video: "🎥"
+};