diff --git a/src/theme/DocCard/index.tsx b/src/theme/DocCard/index.tsx
new file mode 100644
index 00000000..2f7f9343
--- /dev/null
+++ b/src/theme/DocCard/index.tsx
@@ -0,0 +1,126 @@
+import Link from "@docusaurus/Link";
+import {
+ findFirstSidebarItemLink,
+ useDocById,
+} from "@docusaurus/plugin-content-docs/client";
+import { usePluralForm } from "@docusaurus/theme-common";
+import { translate } from "@docusaurus/Translate";
+import clsx from "clsx";
+import { type ReactNode } from "react";
+
+import type {
+ PropSidebarItemCategory,
+ PropSidebarItemLink,
+} from "@docusaurus/plugin-content-docs";
+import type { Props } from "@theme/DocCard";
+import Heading from "@theme/Heading";
+
+import styles from "./styles.module.css";
+
+function useCategoryItemsPlural() {
+ const { selectMessage } = usePluralForm();
+ return (count: number) =>
+ selectMessage(
+ count,
+ translate(
+ {
+ message: "1 item|{count} items",
+ id: "theme.docs.DocCard.categoryDescription.plurals",
+ description:
+ "The default description for a category card in the generated index about how many items this category includes",
+ },
+ { count }
+ )
+ );
+}
+
+function CardContainer({
+ href,
+ children,
+}: {
+ href: string;
+ children: ReactNode;
+}): JSX.Element {
+ return (
+
+ {children}
+
+ );
+}
+
+function CardLayout({
+ href,
+ title,
+ description,
+}: {
+ href: string;
+ title: string;
+ description?: string;
+}): JSX.Element {
+ return (
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ );
+}
+
+function CardCategory({
+ item,
+}: {
+ item: PropSidebarItemCategory;
+}): JSX.Element | null {
+ const href = findFirstSidebarItemLink(item);
+ const categoryItemsPlural = useCategoryItemsPlural();
+
+ // Unexpected: categories that don't have a link have been filtered upfront
+ if (!href) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+function CardLink({ item }: { item: PropSidebarItemLink }): JSX.Element {
+ const doc = useDocById(item.docId ?? undefined);
+ return (
+
+ );
+}
+
+export default function DocCard({ item }: Props): JSX.Element {
+ switch (item.type) {
+ case "link":
+ return ;
+ case "category":
+ return ;
+ default:
+ throw new Error(`unknown item type ${JSON.stringify(item)}`);
+ }
+}
diff --git a/src/theme/DocCard/styles.module.css b/src/theme/DocCard/styles.module.css
new file mode 100644
index 00000000..4f7ad27f
--- /dev/null
+++ b/src/theme/DocCard/styles.module.css
@@ -0,0 +1,27 @@
+.cardContainer {
+ --ifm-link-color: var(--ifm-color-emphasis-800);
+ --ifm-link-hover-color: var(--ifm-color-emphasis-700);
+ --ifm-link-hover-decoration: none;
+
+ box-shadow: 0 1.5px 3px 0 rgb(0 0 0 / 15%);
+ border: 1px solid var(--ifm-color-emphasis-200);
+ transition: all var(--ifm-transition-fast) ease;
+ transition-property: border, box-shadow;
+}
+
+.cardContainer:hover {
+ border-color: var(--ifm-color-primary);
+ box-shadow: 0 3px 6px 0 rgb(0 0 0 / 20%);
+}
+
+.cardContainer *:last-child {
+ margin-bottom: 0;
+}
+
+.cardTitle {
+ font-size: 1.2rem;
+}
+
+.cardDescription {
+ font-size: 0.8rem;
+}