Skip to content

Sidebar Navigation

AppShell provides flexible sidebar navigation with two modes:

  • Auto-generation mode: Automatically generates navigation from module/resource definitions
  • Composition mode: Full control over sidebar structure using declarative components

Quick Start

Auto-generation Mode (Default)

The simplest way to use the sidebar - navigation items are automatically generated from your module definitions:

tsx
import { AppShell, SidebarLayout } from "@tailor-platform/app-shell";

const App = () => (
  <AppShell modules={modules}>
    <SidebarLayout />
  </AppShell>
);

Composition Mode

For full control over sidebar structure, pass children to DefaultSidebar via the sidebar prop:

tsx
import {
  AppShell,
  SidebarLayout,
  DefaultSidebar,
  SidebarItem,
  SidebarGroup,
  SidebarSeparator,
} from "@tailor-platform/app-shell";
import { Package, Settings } from "lucide-react";

const App = () => (
  <AppShell modules={modules}>
    <SidebarLayout
      sidebar={
        <DefaultSidebar>
          <SidebarItem to="/dashboard" />
          <SidebarSeparator />
          <SidebarGroup title="Products" icon={<Package />}>
            <SidebarItem to="/products/all" />
            <SidebarItem to="/products/categories" />
          </SidebarGroup>
          <SidebarItem to="/settings" icon={<Settings />} />
        </DefaultSidebar>
      }
    />
  </AppShell>
);

Components

SidebarItem

A navigation item that automatically resolves title and icon from resource definitions.

Props

PropTypeRequiredDescription
tostringYesTarget URL. External URLs (http://...) are rendered as external links
titlestringNoOverride title. When omitted, auto-resolved from resource meta
iconReactNodeNoOverride icon. When omitted, auto-resolved from resource meta
externalbooleanNoOpens link in new tab with external link icon
renderfunctionNoCustom render function for full UI control

Examples

tsx
// Auto-resolved from resource meta
<SidebarItem to="/dashboard" />

// Override title and icon
<SidebarItem to="/" title="Home" icon={<Home />} />

// External link
<SidebarItem to="https://docs.example.com" external />

// Custom rendering with render prop
<SidebarItem
  to="/tasks"
  render={({ title, icon, isActive }) => (
    <div className={isActive ? "font-bold" : ""}>
      {icon}
      <span>{title}</span>
      <Badge>5</Badge>
    </div>
  )}
/>

Render Prop

When using the render prop, you receive SidebarItemRenderProps:

PropertyTypeDescription
titlestringResolved title (from override or resource meta)
urlstringTarget URL
iconReactNode | undefinedResolved icon
isActivebooleanWhether this item is currently active

SidebarGroup

A collapsible group for organizing navigation items.

Props

PropTypeRequiredDescription
titleLocalizedStringYesGroup title (supports i18n)
iconReactNodeNoGroup icon
tostringNoWhen specified, title becomes a clickable link
defaultOpenbooleanNoInitial expanded state. Default: true
childrenReactNodeYesChild items (SidebarItem, SidebarGroup, etc.)

Examples

tsx
// Basic group
<SidebarGroup title="Products" icon={<Package />}>
  <SidebarItem to="/products/all" />
  <SidebarItem to="/products/categories" />
</SidebarGroup>

// Clickable group header
<SidebarGroup title="Settings" icon={<Settings />} to="/settings">
  <SidebarItem to="/settings/profile" />
  <SidebarItem to="/settings/security" />
</SidebarGroup>

// Initially collapsed
<SidebarGroup title="Archives" defaultOpen={false}>
  <SidebarItem to="/archives/2024" />
  <SidebarItem to="/archives/2023" />
</SidebarGroup>

// Nested groups
<SidebarGroup title="Products" icon={<Package />}>
  <SidebarItem to="/products/all" />
  <SidebarGroup title="Archives" defaultOpen={false}>
    <SidebarItem to="/products/archives/2024" />
    <SidebarItem to="/products/archives/2023" />
  </SidebarGroup>
</SidebarGroup>

i18n Support

Group titles support localized strings:

tsx
import { defineI18nLabels } from "@tailor-platform/app-shell";

const labels = defineI18nLabels({
  en: { products: "Products" },
  ja: { products: "製品" },
});

<SidebarGroup title={labels.t("products")} icon={<Package />}>
  <SidebarItem to="/products/all" />
</SidebarGroup>;

SidebarSeparator

A visual divider between sidebar sections.

Props

This component takes no props.

Example

tsx
<DefaultSidebar>
  <SidebarItem to="/dashboard" />
  <SidebarSeparator />
  <SidebarItem to="/settings" />
</DefaultSidebar>

DefaultSidebar

The main sidebar container that supports both auto-generation and composition modes.

Props

PropTypeRequiredDescription
headerReactNodeNoCustom header content
footerReactNodeNoCustom footer content
childrenReactNodeNoWhen provided, enables composition mode

Examples

tsx
// Auto-generation mode
<DefaultSidebar />

// With custom header and footer
<DefaultSidebar
  header={<Logo />}
  footer={<UserMenu />}
/>

// Composition mode with custom navigation
<DefaultSidebar header={<Logo />}>
  <SidebarItem to="/dashboard" />
  <SidebarGroup title="Products">
    <SidebarItem to="/products/all" />
  </SidebarGroup>
</DefaultSidebar>

Hooks

usePageMeta

A hook to retrieve page metadata (title, icon) for a given URL path.

Signature

typescript
const usePageMeta: (path: string) => PageMeta | null;

type PageMeta = {
  title: string;
  icon?: ReactNode;
};

Parameters

ParameterTypeDescription
pathstringURL path to find meta for (e.g., "/products/all")

Returns

  • PageMeta - Object containing title and optional icon if found
  • null - For external links or when path is not found

Example

tsx
import { usePageMeta } from "@tailor-platform/app-shell";

const MyComponent = () => {
  const pageMeta = usePageMeta("/products/all");

  if (!pageMeta) {
    return <span>Unknown Page</span>;
  }

  return (
    <div>
      {pageMeta.icon}
      <span>{pageMeta.title}</span>
    </div>
  );
};

Access Control

Auto-generation Mode

In auto-generation mode, guards defined in defineModule/defineResource are automatically evaluated. Modules/resources that return hidden() will not appear in the sidebar.

tsx
import { defineModule, defineResource, pass, hidden } from "@tailor-platform/app-shell";

const adminModule = defineModule({
  path: "admin",
  meta: { title: "Admin" },
  // If this guard returns hidden(), the module won't appear in sidebar
  guards: [({ context }) => (context.currentUser.role === "admin" ? pass() : hidden())],
  resources: [
    defineResource({
      path: "users",
      meta: { title: "Users" },
      component: () => <div>Users</div>,
    }),
  ],
});

// Used with auto-generation mode
<DefaultSidebar />;

Composition Mode

In composition mode, use the WithGuard component to control visibility of sidebar items. This allows you to reuse the same guard logic used in route definitions.

tsx
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";

// Define a guard function
const isAdminGuard = ({ context }) => (context.currentUser.role === "admin" ? pass() : hidden());

<DefaultSidebar>
  <SidebarItem to="/dashboard" />

  {/* Wrap items with WithGuard for conditional visibility */}
  <WithGuard guards={[isAdminGuard]}>
    <SidebarGroup title="Admin" icon={<Shield />}>
      <SidebarItem to="/admin/users" />
      <SidebarItem to="/admin/settings" />
    </SidebarGroup>
  </WithGuard>
</DefaultSidebar>;

Curried Guards

For parameterized guards, use the curried pattern:

tsx
// Define a curried guard factory
const hasRole =
  (role: string) =>
  ({ context }) =>
    context.currentUser.role === role ? pass() : hidden();

<DefaultSidebar>
  <WithGuard guards={[hasRole("admin")]}>
    <SidebarItem to="/admin" />
  </WithGuard>

  <WithGuard guards={[hasRole("manager")]}>
    <SidebarItem to="/reports" />
  </WithGuard>
</DefaultSidebar>;

Note: Route-level access control (e.g., redirects) still works via guards in defineResource, even in composition mode. WithGuard only controls visibility—it does not prevent navigation to protected routes.


Migration from Auto-generation

If you're currently using auto-generated navigation and want to customize it:

  1. Keep auto-generation for most items, add custom items where needed:
tsx
// Before: Pure auto-generation
<DefaultSidebar />

// After: Composition with similar structure
<DefaultSidebar>
  <SidebarItem to="/dashboard" />
  <SidebarItem to="/products" />
  <SidebarSeparator />
  <SidebarItem to="https://docs.example.com" title="Documentation" external />
</DefaultSidebar>
  1. Title and icon are auto-resolved from your resource definitions, so you don't need to specify them unless you want to override.

  2. External links can now be added directly in the sidebar.