Appearance
SidebarItem
SidebarItem represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions, supports localized title overrides, and provides active state highlighting.
Import
tsx
import { SidebarItem } from "@tailor-platform/app-shell";Basic Usage
tsx
import { DefaultSidebar, SidebarItem } from "@tailor-platform/app-shell";
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
<SidebarItem to="/orders" />
</DefaultSidebar>;When you only provide to, the title and icon are automatically resolved from the matching resource's meta property.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
to | string | Required | Target URL (internal or external) |
title | LocalizedString | Auto-resolved | Override the display title (i18n supported) |
icon | React.ReactNode | Auto-resolved | Override the icon |
external | boolean | false | Opens link in new tab with external icon |
activeMatch | "exact" | "prefix" | "prefix" | How to match the current path for active state |
render | (props) => ReactNode | - | Custom render function for full control |
Auto-Resolution
Title Resolution
The title is automatically resolved from the resource's meta.title:
tsx
// In your module definition
const dashboardModule = defineModule({
path: "dashboard",
component: DashboardPage,
meta: {
title: "Dashboard", // ← Automatically used
},
});
// In sidebar - no title needed!
<SidebarItem to="/dashboard" />;
// Renders as: DashboardFor the root path (/ or ""), when no meta.title is set, the title falls back to the localized string "Home" (English) or "ホーム" (Japanese).
Icon Resolution
Icons are resolved from the module's meta.icon:
tsx
import { Home, Package } from "lucide-react";
const modules = [
defineModule({
path: "dashboard",
meta: {
title: "Dashboard",
icon: <Home />, // ← Automatically used in sidebar
},
}),
defineModule({
path: "products",
meta: {
title: "Products",
icon: <Package />,
},
}),
];
<DefaultSidebar>
<SidebarItem to="/dashboard" /> {/* Shows Home icon */}
<SidebarItem to="/products" /> {/* Shows Package icon */}
</DefaultSidebar>;Manual Override
Override Title
tsx
<SidebarItem
to="/dashboard"
title="My Dashboard" // Override auto-resolved title
/>Localized Override
Pass a LocalizedString, such as a value from defineI18nLabels, to resolve the override using the current AppShell locale:
tsx
<SidebarItem to="/dashboard" title={labels.t("dashboard")} />Override Icon
tsx
import { Home } from "lucide-react";
<SidebarItem
to="/dashboard"
icon={<Home />} // Override auto-resolved icon
/>;Both Title and Icon
tsx
import { Settings } from "lucide-react";
<SidebarItem to="/settings" title="Settings" icon={<Settings />} />;External Links
Open links in a new tab with an external link icon:
tsx
import { ExternalLink } from "lucide-react";
<SidebarItem
to="https://docs.example.com"
title="Documentation"
icon={<ExternalLink />}
external
/>;The external link icon is automatically added to the end of the item.
Active State Matching
Control how the active state is determined:
Prefix Matching (Default)
Highlights when the current path starts with the to path:
tsx
<SidebarItem to="/products" activeMatch="prefix" />
// Active when:
// - /products
// - /products/123
// - /products/categoriesExact Matching
Highlights only when the current path exactly matches the to path:
tsx
<SidebarItem to="/products" activeMatch="exact" />
// Active when:
// - /products (only)
// NOT active when:
// - /products/123
// - /products/categoriesCustom Rendering
For full control, use the render prop:
tsx
<SidebarItem
to="/notifications"
render={({ title, icon, isActive }) => (
<div className="flex items-center gap-2">
{icon}
<span>{title}</span>
{isActive && <Badge>New</Badge>}
<Badge variant="error">5</Badge>
</div>
)}
/>The render function receives:
typescript
type SidebarItemRenderProps = {
title: string; // Auto-resolved or overridden
url: string; // The `to` prop value
icon?: ReactNode; // Auto-resolved or overridden
isActive: boolean; // Whether this item matches the current route
};Examples
Basic Sidebar
tsx
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
<SidebarItem to="/orders" />
<SidebarItem to="/customers" />
</DefaultSidebar>With Manual Overrides
tsx
import { Home, Package, ShoppingCart, Users } from "lucide-react";
<DefaultSidebar>
<SidebarItem to="/" title="Home" icon={<Home />} />
<SidebarItem to="/products" title="Products" icon={<Package />} />
<SidebarItem to="/orders" title="Orders" icon={<ShoppingCart />} />
<SidebarItem to="/customers" title="Customers" icon={<Users />} />
</DefaultSidebar>;With External Links
tsx
import { HelpCircle, FileText, ExternalLink } from "lucide-react";
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
<SidebarSeparator />
<SidebarItem to="https://docs.example.com" title="Documentation" icon={<FileText />} external />
<SidebarItem to="https://support.example.com" title="Support" icon={<HelpCircle />} external />
</DefaultSidebar>;With Notification Badge
tsx
<SidebarItem
to="/notifications"
render={({ title, icon, isActive }) => (
<>
{icon}
<span>{title}</span>
<Badge variant="error">12</Badge>
</>
)}
/>With Active Indicator
tsx
<SidebarItem
to="/messages"
render={({ title, icon, isActive }) => (
<>
{icon}
<span className={isActive ? "font-bold" : undefined}>{title}</span>
{isActive && <span className="ml-auto">•</span>}
</>
)}
/>Exact Match for Root
tsx
<DefaultSidebar>
<SidebarItem to="/" activeMatch="exact" />
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
</DefaultSidebar>Without activeMatch="exact", the root item would be active on all pages since every path starts with /.
Conditional Rendering with Guards
tsx
import { WithGuard } from "@tailor-platform/app-shell";
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
<WithGuard guards={[isAdmin]}>
<SidebarItem to="/admin" title="Admin Panel" />
</WithGuard>
</DefaultSidebar>;In SidebarGroup
tsx
import { SidebarGroup, SidebarItem } from "@tailor-platform/app-shell";
import { Package } from "lucide-react";
<SidebarGroup title="Products" icon={<Package />}>
<SidebarItem to="/products/all" />
<SidebarItem to="/products/categories" />
<SidebarItem to="/products/brands" />
</SidebarGroup>;Styling
Active State
Active items pick up the sidebar accent background automatically. The class carrying it is internal to the library — don't target it. Override the token instead:
css
:root {
--sidebar-accent: oklch(0.95 0.02 260);
}Custom Styling
Add custom classes via the render prop:
tsx
<SidebarItem
to="/special"
render={({ title, icon, isActive }) => (
<div className={cn("flex items-center gap-2", isActive && "border-l-4 border-primary")}>
{icon}
{title}
</div>
)}
/>Accessibility
- Semantic
<a>tag for links - Proper
hrefattribute - Active state announced to screen readers
- External links open in new tab with
rel="noopener noreferrer" - Keyboard navigation support (Tab, Enter)
Best Practices
Do:
- ✅ Let AppShell auto-resolve title/icon when possible
- ✅ Use
externalfor links outside your app - ✅ Use
exactmatch for root routes - ✅ Group related items with
SidebarGroup - ✅ Keep item labels short (1-2 words)
Don't:
- ❌ Hardcode titles that exist in resource meta
- ❌ Forget
externalfor external links - ❌ Use prefix match for root route (
/) - ❌ Mix internal and external links without visual distinction
- ❌ Create deeply nested navigation (use breadcrumbs instead)
Comparison: Auto vs Manual
| Aspect | Auto-Resolved | Manual Override |
|---|---|---|
| Title | From resource meta.title | Specify title prop |
| Icon | From module meta.icon | Specify icon prop |
| Effort | Less code, DRY | More flexible |
| Maintenance | Single source of truth | Must update in 2 places |
| Use When | Standard navigation | Custom items, external links |
TypeScript
Full type safety:
typescript
import { type SidebarItemProps, type SidebarItemRenderProps } from "@tailor-platform/app-shell";
// Type-safe props
const itemProps: SidebarItemProps = {
to: "/dashboard",
title: "Dashboard",
activeMatch: "prefix",
};
// Type-safe render function
const customRender = (props: SidebarItemRenderProps) => {
// props.title, props.icon, props.isActive are all typed
return <div>{props.title}</div>;
};Troubleshooting
Title/Icon not showing
Problem: SidebarItem doesn't display title or icon
Solutions:
- Check that the resource has
meta.titledefined - Verify the
toprop matches the resource path exactly - Ensure the module has
meta.iconfor icon display - Try manually specifying
titleandiconprops
Active state not working
Problem: Item isn't highlighted when on that page
Solutions:
- Check that the
toprop matches the current route - Use
exactmatch for root routes (/) - Use
prefixmatch for parent routes with children - Verify you're using React Router's
<Link>correctly
External link not opening in new tab
Problem: External link opens in same tab
Solution: Add external prop:
tsx
<SidebarItem to="https://..." external />Related Components
- SidebarGroup - Group multiple items
- DefaultSidebar - Container for sidebar items
- WithGuard - Conditional rendering
Related Concepts
- Sidebar Navigation - Sidebar customization guide
- Modules and Resources - Define routes and meta
API Reference
- usePageMeta - Hook for resolving page metadata