Skip to content
    Style: Wireframe

    Blocks · Navigation

    Icon sidebar

    An app sidebar: product mark, links with icons, the current one marked with aria-current, settings pinned at the bottom.

    13% of the 2,073 sites in The UI Index have a sidebar (261 sites).

    Overview

    Three deploys went live today and none needed a rollback. Open the Activity tab to see who shipped what.

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/icon-sidebar.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/icon-sidebar.json

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    icon-sidebar.classes.ts
    import type { IconName } from '@/components/ui/icon';
    
    /** An app sidebar: the product mark at the top, a list of links each with an icon, the current one marked with
     *  aria-current="page", a settings link pinned to the bottom. It fills the height of the space it is given (at
     *  least 26rem). For a docs sidebar (text links under headings) see the sidebar-nav item. */
    export const iconSidebar = {
      root: 'w-full',
      aside: 'flex h-full min-h-[26rem] min-w-0 flex-col gap-6 bg-muted/40 p-4 text-card-foreground',
      brand: 'flex min-h-10 items-center gap-2 rounded-md px-3 text-base font-semibold text-foreground transition-colors hover:bg-accent active:bg-pressed',
      brandMark: 'grid size-6 shrink-0 place-items-center rounded-md bg-primary text-primary-foreground',
      nav: 'flex min-w-0 flex-1 flex-col gap-1',
      list: 'm-0 flex list-none flex-col gap-1 p-0',
      link: 'flex min-h-10 w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed aria-[current=page]:bg-accent aria-[current=page]:text-foreground aria-[current=page]:hover:bg-input/30 aria-[current=page]:active:bg-pressed',
      icon: 'shrink-0',
      label: 'min-w-0 flex-1 text-left',
      footer: 'mt-auto flex flex-col gap-1 border-t pt-4',
    };
    
    /** The product mark's glyph on the 16px grid (a four-point star; the free icon set has none). */
    export const MARK_PATH = 'M8 2l1.8 4.2L14 8l-4.2 1.8L8 14l-1.8-4.2L2 8l4.2-1.8z';
    
    export type SidebarLink = {
      label: string;
      href: string;
      icon: IconName;
      /** The page the visitor is on. One link in the whole sidebar. */
      current?: boolean;
    };
    export type IconSidebarContent = {
      /** The navigation's accessible name. Default: Main. */
      label?: string;
      brand: { name: string; href: string };
      links: SidebarLink[];
      /** Pinned to the bottom (Settings). */
      footer?: SidebarLink[];
      id?: string;
    };
    icon-sidebar.tsx
    import { Icon } from '@/components/ui/icon';
    import { cn } from '@/lib/utils';
    import { iconSidebar as c, MARK_PATH, type IconSidebarContent, type SidebarLink } from './icon-sidebar.classes';
    
    export type IconSidebarProps = IconSidebarContent & { className?: string };
    
    const row = (l: SidebarLink) => (
      <li key={l.href}>
        <a href={l.href} aria-current={l.current ? 'page' : undefined} className={c.link}>
          <Icon name={l.icon} className={c.icon} />
          <span className={c.label}>{l.label}</span>
        </a>
      </li>
    );
    
    /** An app sidebar: product mark, links with icons (the current one aria-current), settings pinned at the bottom. */
    export function IconSidebar({ label = 'Main', brand, links, footer, id, className }: IconSidebarProps) {
      return (
        <div id={id} className={cn(c.root, className)}>
          <aside className={c.aside}>
            <a href={brand.href} className={c.brand}>
              <span className={c.brandMark} aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="size-4"><path d={MARK_PATH} /></svg></span>
              <span>{brand.name}</span>
            </a>
            <nav aria-label={label} className={c.nav}>
              <ul className={c.list}>{links.map(row)}</ul>
              {footer?.length ? (
                <div className={c.footer}>
                  <ul className={c.list}>{footer.map(row)}</ul>
                </div>
              ) : null}
            </nav>
          </aside>
        </div>
      );
    }