Skip to main content
Urbicon UI
Back to Recipes

Dashboard Layout

App-shell pattern built on SidebarLayout — persistent sidebar on desktop, slide-in overlay on mobile, with stat cards, chart, and activity feed.

Live Preview

Acme Inc

Dashboard

Welcome back, Mara

Total Users

12,847 +12.5%
Target: 16,500 users by Q2

Revenue

$48,392 +8.2%
Target: $75,000 monthly

Active Sessions

1,429 -3.1%
Down from 1,475 last week

Conversion

3.24% +0.8%
Target: 4.0% conversion rate

Revenue Overview

Revenue — Jan: 40Revenue — Feb: 65Revenue — Mar: 45Revenue — Apr: 80Revenue — May: 55Revenue — Jun: 70Revenue — Jul: 85Revenue — Aug: 60Revenue — Sep: 90Revenue — Oct: 75Revenue — Nov: 95Revenue — Dec: 88
Monthly revenue
CategoryRevenue
Jan40
Feb65
Mar45
Apr80
May55
Jun70
Jul85
Aug60
Sep90
Oct75
Nov95
Dec88

Recent Activity

SC

Sarah Chen

Purchased Pro plan · 2 min ago

MR

Marcus Rivera

Submitted support ticket · 15 min ago

AP

Aisha Patel

Updated profile settings · 1 hour ago

TW

Tom Weber

Exported analytics report · 2 hours ago

LK

Lisa Kim

Added team member · 3 hours ago

Key Features

  • SidebarLayout as the app shell — responsive sidebar with mobile hamburger overlay
  • Stat cards with equal height and baseline-aligned values
  • Tooltip with explicit placement to avoid Floating-UI drift on tight grids
  • Revenue chart with consistent month-label spacing
  • Activity feed stacked full-width under the chart
  • Active-route highlighting on sidebar navigation

Code

Dashboard Layout Recipe

<script lang="ts">
  import {
    Avatar, Badge, BarChart, Button, Card, Progress, Tooltip,
    SidebarLayout, MenuIcon, HomeIcon, BarChartIcon, UsersIcon, SettingsIcon
  } from '@urbicon-ui/blocks';

  let sidebarOpen = $state(false);
  let activeRoute = $state('dashboard');

  const navItems = [
    { id: 'dashboard', label: 'Dashboard', icon: HomeIcon },
    { id: 'analytics', label: 'Analytics', icon: BarChartIcon },
    { id: 'users', label: 'Users', icon: UsersIcon },
    { id: 'settings', label: 'Settings', icon: SettingsIcon }
  ];

  const stats = [
    { label: 'Total Users', value: '12,847', change: '+12.5%', intent: 'success' as const, progress: 78, tooltip: 'Target: 16,500 users by Q2' },
    { label: 'Revenue', value: '$48,392', change: '+8.2%', intent: 'success' as const, progress: 64, tooltip: 'Target: $75,000 monthly' },
    { label: 'Active Sessions', value: '1,429', change: '-3.1%', intent: 'danger' as const, progress: 45, tooltip: 'Down from 1,475 last week' },
    { label: 'Conversion', value: '3.24%', change: '+0.8%', intent: 'success' as const, progress: 81, tooltip: 'Target: 4.0% conversion rate' }
  ];

  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  const chartData = [40, 65, 45, 80, 55, 70, 85, 60, 90, 75, 95, 88];
  const revenue = months.map((label, i) => ({ label, values: [chartData[i]] }));

  const recentActivity = [
    { user: 'Sarah Chen', action: 'Purchased Pro plan', time: '2 min ago' },
    { user: 'Marcus Rivera', action: 'Submitted support ticket', time: '15 min ago' },
    { user: 'Aisha Patel', action: 'Updated profile settings', time: '1 hour ago' },
    { user: 'Tom Weber', action: 'Exported analytics report', time: '2 hours ago' },
    { user: 'Lisa Kim', action: 'Added team member', time: '3 hours ago' }
  ];
</script>

<SidebarLayout bind:open={sidebarOpen} sidebarWidth="14rem" data-design-pattern="dashboard">
  {#snippet sidebarHeader()}
    <a href="/" class="flex h-14 items-center px-4 font-bold text-primary">Acme Inc</a>
  {/snippet}

  {#snippet sidebar()}
    <nav aria-label="Dashboard sidebar" class="flex flex-col gap-1 p-3">
      {#each navItems as item (item.id)}
        {@const Icon = item.icon}
        <button
          type="button"
          onclick={() => (activeRoute = item.id)}
          class={activeRoute === item.id
            ? 'flex items-center gap-2.5 rounded-lg bg-primary-subtle px-3 py-2 text-sm font-medium text-primary'
            : 'flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary'}
        >
          <Icon class="h-4 w-4 shrink-0" />
          <span>{item.label}</span>
        </button>
      {/each}
    </nav>
  {/snippet}

  {#snippet sidebarFooter()}
    <div class="flex items-center gap-3 p-3">
      <Avatar name="Mara Cohen" size="sm" />
      <div class="min-w-0">
        <div class="truncate text-xs font-medium text-text-primary">Mara C</div>
        <div class="truncate text-3xs text-text-tertiary">Admin</div>
      </div>
    </div>
  {/snippet}

  {#snippet mobileHeader({ openSidebar })}
    <Button variant="ghost" size="sm" onclick={openSidebar} aria-label="Open menu">
      <MenuIcon class="h-5 w-5" />
    </Button>
    <span class="font-semibold">Acme Inc</span>
  {/snippet}

  <div class="space-y-6">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <div>
        <h1 class="text-2xl font-bold text-text-primary">Dashboard</h1>
        <p class="text-sm text-text-tertiary">Welcome back, Mara</p>
      </div>
      <Button size="sm" intent="primary">Export Report</Button>
    </div>

    <div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
      {#each stats as stat (stat.label)}
        <Card class="h-full" variant="elevated">
          <Tooltip label={stat.tooltip} placement="top">
            <div class="flex flex-col gap-2">
              <p class="min-h-8 text-xs font-medium text-text-tertiary">{stat.label}</p>
              <div class="flex items-baseline gap-2">
                <span class="text-2xl font-bold tabular-nums text-text-primary">{stat.value}</span>
                <Badge variant="soft" intent={stat.intent} size="sm">{stat.change}</Badge>
              </div>
              <Progress
                value={stat.progress}
                size="xs"
                intent={stat.intent === 'danger' ? 'danger' : 'primary'}
              />
            </div>
          </Tooltip>
        </Card>
      {/each}
    </div>

    <Card>
      <div class="space-y-4">
        <h2 class="text-sm font-semibold text-text-primary">Revenue Overview</h2>
        <BarChart
          data={revenue}
          series={[{ label: 'Revenue' }]}
          height={192}
          showLegend={false}
          ariaLabel="Monthly revenue"
        />
      </div>
    </Card>

    <Card>
      <div class="space-y-4">
        <h2 class="text-sm font-semibold text-text-primary">Recent Activity</h2>
        <div class="space-y-3">
          {#each recentActivity as activity (activity.user)}
            <div class="flex items-start gap-3">
              <Avatar name={activity.user} size="sm" />
              <div class="min-w-0 flex-1">
                <p class="truncate text-sm font-medium text-text-primary">{activity.user}</p>
                <p class="text-xs text-text-tertiary">{activity.action} · {activity.time}</p>
              </div>
            </div>
          {/each}
        </div>
      </div>
    </Card>
  </div>
</SidebarLayout>