SidebarItem
SidebarItem represents an individual navigation link in the sidebar. It automatically resolves titles and icons from resource definitions and provides active state highlighting.
Import
import { SidebarItem } from "@tailor-platform/app-shell";Basic Usage
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 | string | Auto-resolved | Override the display title |
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:
// 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: DashboardIcon Resolution
Icons are resolved from the module's meta.icon:
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
<SidebarItem
to="/dashboard"
title="My Dashboard" // Override auto-resolved title
/>Override Icon
import { Home } from "lucide-react";
<SidebarItem
to="/dashboard"
icon={<Home />} // Override auto-resolved icon
/>;Both Title and Icon
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:
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:
<SidebarItem to="/products" activeMatch="prefix" />
// Active when:
// - /products
// - /products/123
// - /products/categoriesExact Matching
Highlights only when the current path exactly matches the to path:
<SidebarItem to="/products" activeMatch="exact" />
// Active when:
// - /products (only)
// NOT active when:
// - /products/123
// - /products/categoriesCustom Rendering
For full control, use the render prop:
<SidebarItem
to="/notifications"
render={({ title, icon, isActive }) => (
<div className="astw:flex astw:items-center astw:gap-2">
{icon}
<span>{title}</span>
{isActive && <Badge>New</Badge>}
<Badge variant="error">5</Badge>
</div>
)}
/>The render function receives:
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
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/products" />
<SidebarItem to="/orders" />
<SidebarItem to="/customers" />
</DefaultSidebar>With Manual Overrides
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
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
<SidebarItem
to="/notifications"
render={({ title, icon, isActive }) => (
<>
{icon}
<span>{title}</span>
<Badge variant="error">12</Badge>
</>
)}
/>With Active Indicator
<SidebarItem
to="/messages"
render={({ title, icon, isActive }) => (
<>
{icon}
<span className={isActive ? "astw:font-bold" : undefined}>{title}</span>
{isActive && <span className="astw:ml-auto">•</span>}
</>
)}
/>Exact Match for Root
<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
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
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 automatically receive the astw:bg-sidebar-accent background class:
/* Active item styling (automatic) */
.astw:bg-sidebar-accent {
background: var(--sidebar-accent);
font-weight: 500;
}Custom Styling
Add custom classes via the render prop:
<SidebarItem
to="/special"
render={({ title, icon, isActive }) => (
<div
className={cn(
"astw:flex astw:items-center astw:gap-2",
isActive && "astw:border-l-4 astw: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:
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:
<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