diff --git a/src/ui/sidebar.test.tsx b/src/ui/sidebar.test.tsx
new file mode 100644
index 00000000..2c86e95f
--- /dev/null
+++ b/src/ui/sidebar.test.tsx
@@ -0,0 +1,26 @@
+import { expect, test } from "bun:test";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+
+import { SidebarItem } from "./sidebar.js";
+
+test("asChild renders the consumer's link with the icon, label and count inside it", () => {
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ } count={3}>
+ Inbox
+ ,
+ );
+ });
+ const link = host.querySelector("a");
+ expect(link?.getAttribute("href")).toBe("/inbox");
+ expect(link?.getAttribute("aria-current")).toBe("page");
+ expect(link?.getAttribute("data-slot")).toBe("sidebar-item");
+ expect(link?.querySelector("svg")).not.toBeNull();
+ expect(link?.textContent).toBe("Inbox3");
+ act(() => root.unmount());
+ host.remove();
+});
diff --git a/src/ui/sidebar.tsx b/src/ui/sidebar.tsx
index a160c75b..ef6aef0e 100644
--- a/src/ui/sidebar.tsx
+++ b/src/ui/sidebar.tsx
@@ -1,5 +1,6 @@
import { Slot } from "@radix-ui/react-slot";
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
+import { cloneElement, isValidElement } from "react";
import type * as React from "react";
import { cn } from "../lib/utils.js";
@@ -114,6 +115,33 @@ export type SidebarItemProps = React.ComponentProps<"a"> & {
asChild?: boolean;
};
+function sidebarItemParts(
+ icon: React.ReactNode,
+ label: React.ReactNode,
+ count: number | undefined,
+) {
+ return (
+ <>
+ {icon === undefined ? null : (
+
+ {icon}
+
+ )}
+
+ {label}
+
+ {count === undefined ? null : (
+
+ {count}
+
+ )}
+ >
+ );
+}
+
export function SidebarItem({
active = false,
icon,
@@ -124,6 +152,16 @@ export function SidebarItem({
...props
}: SidebarItemProps) {
const Comp = asChild ? Slot : "a";
+ // Slot accepts one child, so with `asChild` the icon, label and count go
+ // inside the consumer's element (a router link) rather than beside it.
+ const content =
+ asChild && isValidElement<{ children?: React.ReactNode }>(children)
+ ? cloneElement(
+ children,
+ undefined,
+ sidebarItemParts(icon, children.props.children, count),
+ )
+ : sidebarItemParts(icon, children, count);
return (
- {icon === undefined ? null : (
-
- {icon}
-
- )}
-
- {children}
-
- {count === undefined ? null : (
-
- {count}
-
- )}
+ {content}
);