# Sidebar Double-Sided Mail

> A sidebar double-sided mail block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/sidebar-04).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/sidebar-04
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/sidebar-04-light-f123839abb9f44cd.png
- Registry item: https://blocks.so/r/sidebar-04.json
- Categories: sidebar
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/sidebar-04.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`, `lucide-react`
- Registry items: `avatar`, `dropdown-menu`, `sidebar`

## Files

### `content/components/sidebar/sidebar-04/app/page.tsx`

```tsx
'use client';

import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/sidebar-04/app-sidebar';
import { MailProvider, useMail } from '@/components/sidebar-04/mail-context';

function MailPreview() {
  const { selectedMail } = useMail();
  if (!selectedMail) {
    return (
      <div className="flex h-full items-center justify-center text-muted-foreground text-sm">
        Select an email to preview
      </div>
    );
  }
  return (
    <div className="flex h-full flex-col">
      <div className="border-b p-4">
        <div className="font-semibold text-lg">{selectedMail.subject}</div>
        <div className="text-muted-foreground text-sm">
          From: {selectedMail.name} ({selectedMail.email})
        </div>
      </div>
      <div className="whitespace-pre-wrap p-4 text-sm">{`${selectedMail.teaser}\n\nThis is a sample message body for the selected email.\nIt demonstrates the preview area on the right side.`}</div>
    </div>
  );
}

export default function Page() {
  return (
    <SidebarProvider>
      <MailProvider>
        <div className="flex h-dvh w-full">
          <AppSidebar />
          <SidebarInset>
            <div className="m-2 flex-1 rounded-xl border">
              <MailPreview />
            </div>
          </SidebarInset>
        </div>
      </MailProvider>
    </SidebarProvider>
  );
}
```

### `content/components/sidebar/sidebar-04/app-sidebar.tsx`

```tsx
'use client';

import {
  IconBrandAmongUs,
  IconCarambola,
  IconHourglassHigh,
  IconMailbox,
} from '@tabler/icons-react';
import { Archive, Flag } from 'lucide-react';
import * as React from 'react';
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  useSidebar,
} from '@/components/ui/sidebar';
import { useMail } from '@/components/sidebar-04/mail-context';
import { NavUser } from '@/components/sidebar-04/nav-user';

// This is sample data
const data = {
  user: {
    name: 'ephraim',
    email: 'ephraim@blocks.so',
    avatar: '/avatar-01.png',
  },
  navMain: [
    {
      title: 'Inbox',
      url: '#',
      icon: IconMailbox,
      isActive: true,
    },
    {
      title: 'Starred',
      url: '#',
      icon: IconCarambola,
      isActive: false,
    },
    {
      title: 'Important',
      url: '#',
      icon: Flag,
      isActive: false,
    },
    {
      title: 'Scheduled',
      url: '#',
      icon: IconHourglassHigh,
      isActive: false,
    },
    {
      title: 'Archive',
      url: '#',
      icon: Archive,
      isActive: false,
    },
  ],
  labels: [
    { title: 'Personal', color: 'bg-green-400 dark:bg-green-300' },
    { title: 'Work', color: 'bg-blue-400 dark:bg-blue-300' },
    { title: 'Travel', color: 'bg-orange-400 dark:bg-orange-300' },
    { title: 'Receipts', color: 'bg-purple-400 dark:bg-purple-300' },
  ],
  mails: [
    {
      name: 'Nora Patel',
      email: 'nora@acme.co',
      subject: 'Welcome to Acme Mail',
      date: '08:15 AM',
      teaser:
        "Hi there — here's a quick tour of your new inbox.\nPin, label, and schedule messages to stay organized.",
    },
    {
      name: 'Stripe',
      email: 'no-reply@stripe.com',
      subject: 'Your payout has arrived',
      date: 'Yesterday',
      teaser:
        'A payout of $3,245.90 was sent to your bank account.\nView details in your dashboard.',
    },
    {
      name: 'GitHub',
      email: 'noreply@github.com',
      subject: 'New activity on acme/app',
      date: 'Tue',
      teaser:
        '3 pull requests need your review. CI passed on main.\nClick to open the review queue.',
    },
    {
      name: 'Ava Chen',
      email: 'ava.chen@example.com',
      subject: 'Agenda for Friday standup',
      date: 'Mon',
      teaser:
        "Let's cover onboarding, billing bugs, and Q4 goals.\nReply with anything you want to add.",
    },
    {
      name: 'Figma',
      email: 'updates@figma.com',
      subject: "What's new in Figma",
      date: 'Sep 12',
      teaser:
        'Variables, auto layout improvements, and dev mode updates.\nWatch the recap to learn more.',
    },
    {
      name: 'Linear',
      email: 'bot@linear.app',
      subject: '[ACME-432] Edit modal broken',
      date: 'Sep 11',
      teaser:
        'Issue created by Wendy. Repro steps included.\nSeverity: high, priority: P1.',
    },
    {
      name: 'Airbnb',
      email: 'booking@airbnb.com',
      subject: 'Your trip to Kyoto',
      date: 'Sep 10',
      teaser:
        'Get ready for your stay. Check-in details and local recommendations inside.',
    },
    {
      name: 'Notion',
      email: 'team@notion.so',
      subject: 'Weekly team recap',
      date: 'Sep 09',
      teaser:
        'Marketing shipped pricing page revamp. Eng closed 14 issues.\nSee full notes in the doc.',
    },
    {
      name: 'Google Calendar',
      email: 'calendar@google.com',
      subject: 'Design sync moved',
      date: 'Sep 09',
      teaser:
        "Event 'Design sync' moved to 3:30 PM. Meet link updated in the invite.",
    },
    {
      name: 'HN Daily',
      email: 'digest@ycombinator.com',
      subject: 'Top stories today',
      date: 'Sep 08',
      teaser:
        'OpenAI releases new API, SQLite 3.46, and why we left Kubernetes.\nRead the full digest.',
    },
  ],
};

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
  // Note: I'm using state to show active item.
  // IRL you should use the url/router.
  const [activeItem, setActiveItem] = React.useState(data.navMain[0]);
  const [mails, setMails] = React.useState(data.mails);
  const [query, setQuery] = React.useState('');
  const { setOpen } = useSidebar();
  const { setSelectedMail } = useMail();

  const filteredMails = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return mails;
    return mails.filter((m) =>
      [m.name, m.email, m.subject, m.teaser]
        .join('\n')
        .toLowerCase()
        .includes(q)
    );
  }, [mails, query]);

  return (
    <div className="flex">
      {/* This is the first sidebar */}
      <Sidebar
        className="border-r p-2 px-1"
        collapsible="none"
        style={{ '--sidebar-width': '12rem' } as React.CSSProperties}
      >
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton
                className="md:h-8 md:p-0"
                render={<a href="#" />}
                size="lg"
              >
                <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
                  <IconBrandAmongUs className="size-4" />
                </div>
                <div className="grid flex-1 text-left text-sm leading-tight">
                  <span className="truncate font-medium">Acme Inc</span>
                  <span className="truncate text-xs">Enterprise</span>
                </div>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent className="px-1.5 md:px-0">
              <SidebarMenu>
                {data.navMain.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton
                      className="px-2.5 md:px-2"
                      isActive={activeItem?.title === item.title}
                      onClick={() => {
                        setActiveItem(item);
                        const mail = data.mails.sort(() => Math.random() - 0.5);
                        setMails(
                          mail.slice(
                            0,
                            Math.max(5, Math.floor(Math.random() * 10) + 1)
                          )
                        );
                        setOpen(true);
                      }}
                    >
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            <SidebarGroupLabel className="px-1.5 text-xs md:px-0">
              Labels
            </SidebarGroupLabel>
            <SidebarGroupContent className="px-1.5 md:px-0">
              <SidebarMenu>
                {data.labels.map((label) => (
                  <SidebarMenuItem key={label.title}>
                    <SidebarMenuButton
                      className="px-2.5 md:px-2"
                      render={<div className="flex items-center gap-3" />}
                    >
                      <div className={`h-3 w-3 rounded ${label.color}`} />
                      <span>{label.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <NavUser user={data.user} />
        </SidebarFooter>
      </Sidebar>

      {/* This is the second sidebar */}
      {/* We disable collapsible and let it fill remaining space */}
      <Sidebar
        className="hidden min-w-96 flex-1 border-r md:flex"
        collapsible="none"
      >
        <SidebarHeader className="gap-3.5 border-b p-4">
          <div className="flex w-full items-center justify-between">
            <div className="font-medium text-base text-foreground">
              {activeItem?.title}
            </div>
          </div>
          <SidebarInput
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Type to search..."
            value={query}
          />
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup className="px-0 pt-0">
            <SidebarGroupContent>
              {filteredMails.length === 0 && (
                <div className="p-4 text-muted-foreground text-sm">
                  No results
                </div>
              )}
              {filteredMails.map((mail) => (
                <a
                  className="flex flex-col items-start gap-2 whitespace-nowrap border-b p-4 text-sm leading-tight hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
                  href="#"
                  key={mail.email}
                  onClick={(e) => {
                    e.preventDefault();
                    setSelectedMail(mail);
                  }}
                >
                  <div className="flex w-full items-center gap-2">
                    <span>{mail.name}</span>{' '}
                    <span className="ml-auto text-xs">{mail.date}</span>
                  </div>
                  <span className="font-medium">{mail.subject}</span>
                  <span className="line-clamp-2 w-[260px] whitespace-break-spaces text-xs">
                    {mail.teaser}
                  </span>
                </a>
              ))}
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
    </div>
  );
}
```

### `content/components/sidebar/sidebar-04/nav-user.tsx`

```tsx
'use client';

import {
  BadgeCheck,
  Bell,
  ChevronsUpDown,
  CreditCard,
  LogOut,
  Sparkles,
} from 'lucide-react';

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  useSidebar,
} from '@/components/ui/sidebar';

export function NavUser({
  user,
}: {
  user: {
    name: string;
    email: string;
    avatar: string;
  };
}) {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton
                className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground md:h-8 md:p-0"
                size="lg"
              />
            }
          >
            <Avatar className="h-8 w-8 rounded-lg">
              <AvatarImage alt={user.name} src={user.avatar} />
              <AvatarFallback className="rounded-lg">CN</AvatarFallback>
            </Avatar>
            <div className="grid flex-1 text-left text-sm leading-tight">
              <span className="truncate font-medium">{user.name}</span>
              <span className="truncate text-xs">{user.email}</span>
            </div>
            <ChevronsUpDown className="ml-auto size-4" />
          </DropdownMenuTrigger>
          <DropdownMenuContent
            align="end"
            className="w-(--anchor-width) min-w-56 rounded-lg"
            side={isMobile ? 'bottom' : 'right'}
            sideOffset={4}
          >
            <DropdownMenuLabel className="p-0 font-normal">
              <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
                <Avatar className="h-8 w-8 rounded-lg">
                  <AvatarImage alt={user.name} src={user.avatar} />
                  <AvatarFallback className="rounded-lg">CN</AvatarFallback>
                </Avatar>
                <div className="grid flex-1 text-left text-sm leading-tight">
                  <span className="truncate font-medium">{user.name}</span>
                  <span className="truncate text-xs">{user.email}</span>
                </div>
              </div>
            </DropdownMenuLabel>
            <DropdownMenuSeparator />
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <Sparkles />
                Upgrade to Pro
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <BadgeCheck />
                Account
              </DropdownMenuItem>
              <DropdownMenuItem>
                <CreditCard />
                Billing
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Bell />
                Notifications
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <LogOut />
              Log out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}
```

### `content/components/sidebar/sidebar-04/mail-context.tsx`

```tsx
'use client';

import * as React from 'react';

export type Mail = {
  name: string;
  email: string;
  subject: string;
  date: string;
  teaser: string;
  labels?: string[];
};

type MailContextValue = {
  selectedMail: Mail | null;
  setSelectedMail: (mail: Mail | null) => void;
};

const MailContext = React.createContext<MailContextValue | undefined>(
  undefined
);

export function MailProvider({ children }: { children: React.ReactNode }) {
  const [selectedMail, setSelectedMail] = React.useState<Mail | null>(null);
  const value = React.useMemo(
    () => ({ selectedMail, setSelectedMail }),
    [selectedMail]
  );
  return <MailContext.Provider value={value}>{children}</MailContext.Provider>;
}

export function useMail() {
  const ctx = React.useContext(MailContext);
  if (!ctx) throw new Error('useMail must be used within MailProvider');
  return ctx;
}
```
