Customization

Last updated on 2026-05-31

The SaaS Metrics Kit is designed to be customized for your SaaS analytics product or internal dashboard. All components use design tokens and Tailwind CSS utilities, making brand adaptation straightforward.

Changing Colors

Update CSS custom properties in app/globals.css. The default theme uses oklch hue 260 (blue-violet). Change the hue to rebrand the entire kit:

:root {
  /* Change from blue-violet (hue 260) to green (hue 150) */
  --primary: oklch(0.45 0.2 150);
  --primary-foreground: oklch(0.98 0 0);
  --accent: oklch(0.94 0.02 150);
  --accent-foreground: oklch(0.35 0.15 150);
  --ring: oklch(0.55 0.18 150);
}

.dark {
  --primary: oklch(0.7 0.18 150);
  --primary-foreground: oklch(0.15 0.02 150);
}

All 25 screens automatically inherit the new colors -- buttons, badges, links, charts, sidebar accents, status indicators, and focus rings all update at once.

Changing the Secondary Color

The secondary color uses a separate hue (default 71, golden). To change it:

:root {
  /* Change from gold (hue 71) to magenta (hue 330) */
  --secondary: oklch(0.78 0.14 330);
  --secondary-foreground: oklch(0.25 0.05 330);
}

Changing Chart Colors

Update the five chart tokens to match your brand palette:

:root {
  --chart-1: oklch(0.55 0.2 150);   /* Primary chart color */
  --chart-2: oklch(0.75 0.14 330);  /* Secondary chart color */
  --chart-3: oklch(0.65 0.2 220);   /* Tertiary */
  --chart-4: oklch(0.6 0.18 40);    /* Quaternary */
  --chart-5: oklch(0.7 0.15 280);   /* Quinary */
}

All 10+ chart types across all analytics screens will update automatically.

Changing Typography

Configure fonts in app/layout.tsx:

import { Inter, DM_Sans, JetBrains_Mono } from "next/font/google";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const dmSans = DM_Sans({ subsets: ["latin"], variable: "--font-dm-sans" });
const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains" });

Swap these for any Google Font or local font files. The CSS variables --font-sans, --font-heading, and --font-mono control body text, headings, and code respectively.

Replacing Seed Data

All mock data lives in data/seed.ts. Replace it with your data source:

API calls

import { getRevenueHistory } from "@/lib/api"

export default async function RevenuePage() {
  const revenueData = await getRevenueHistory()
  return <RevenueChart data={revenueData} />
}

Billing provider (Stripe)

// app/api/stripe/mrr/route.ts
import Stripe from "stripe"

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function GET() {
  const subscriptions = await stripe.subscriptions.list({
    status: "active",
    limit: 100,
    expand: ["data.customer"],
  })

  const mrr = subscriptions.data.reduce((sum, sub) => {
    return sum + sub.items.data.reduce((itemSum, item) => {
      return itemSum + (item.price.unit_amount ?? 0) * (item.quantity ?? 1)
    }, 0)
  }, 0) / 100

  return Response.json({ mrr })
}

Database (Prisma, Drizzle)

import { db } from "@/lib/db"

const customers = await db.customer.findMany({
  orderBy: { mrr: "desc" },
  include: { subscription: true },
  take: 10,
})

Analytics platform (Mixpanel, Amplitude)

// app/api/analytics/funnel/route.ts
export async function GET() {
  const res = await fetch("https://mixpanel.com/api/2.0/funnels", {
    headers: { Authorization: `Basic ${btoa(process.env.MIXPANEL_SECRET + ":")}` },
  })
  const data = await res.json()
  return Response.json(data)
}

Extending Components

Use the cn() utility (from lib/utils.ts) to add custom classes without conflicts:

import { Button } from "@/components/ui/button"

<Button className="rounded-full shadow-lg" size="lg">
  Export Report
</Button>

Adding Component Variants

Components use class-variance-authority for variant management:

const badgeVariants = cva("...", {
  variants: {
    variant: {
      default: "...",
      success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
      warning: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200",
    },
  },
})

Customizing the Sidebar

Edit components/layout/app-sidebar.tsx to add or remove navigation items. The sidebar reads from the navSections array in data/seed.ts:

export const analyticsNavItems: NavItem[] = [
  { label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard" },
  { label: "Revenue", href: "/revenue", icon: "DollarSign" },
  { label: "Customers", href: "/customers", icon: "Users" },
  // Remove items you don't need
  // Add items for new analytics pages
]

Each item takes a label, href, and a Lucide icon name string. Active state highlighting is handled automatically based on the current pathname.

Customizing the Header

Edit components/layout/app-header.tsx to modify the top bar:

  • Theme toggle -- switches between light and dark mode
  • User menu -- dropdown with profile, settings, and sign out
  • Additional actions -- add search, notifications, or custom quick actions

Remove or reorder these actions to fit your dashboard workflow.

Adding New Dashboard Pages

  1. Create a new route in app/(dashboard)/your-page/page.tsx
  2. The page automatically inherits the dashboard layout (sidebar, header)
  3. Add a sidebar link in data/seed.ts under the appropriate nav section
// app/(dashboard)/cohorts/page.tsx
export default function CohortsPage() {
  return (
    <div className="space-y-6">
      <PageHeader
        title="Cohort Analysis"
        description="Deep dive into customer cohort behavior"
        breadcrumbs={[
          { label: "Dashboard", href: "/dashboard" },
          { label: "Cohorts" },
        ]}
      />
      {/* Your content */}
    </div>
  )
}

Integrating Billing Backends

Stripe

// app/api/stripe/subscriptions/route.ts
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function GET() {
  const subscriptions = await stripe.subscriptions.list({
    status: "active",
    limit: 100,
    expand: ["data.customer"],
  })
  return Response.json(
    subscriptions.data.map((sub) => ({
      id: sub.id,
      customerName: (sub.customer as Stripe.Customer).name,
      plan: sub.items.data[0].price.nickname,
      mrr: (sub.items.data[0].price.unit_amount ?? 0) / 100,
      status: sub.status,
      startDate: new Date(sub.start_date * 1000).toISOString(),
    }))
  )
}

Chargebee

// app/api/chargebee/mrr/route.ts
export async function GET() {
  const res = await fetch(
    `https://${process.env.CHARGEBEE_SITE}.chargebee.com/api/v2/subscriptions`,
    {
      headers: {
        Authorization: `Basic ${btoa(process.env.CHARGEBEE_API_KEY + ":")}`,
      },
    }
  )
  const data = await res.json()
  return Response.json(data.list)
}

Paddle

// app/api/paddle/subscriptions/route.ts
export async function GET() {
  const res = await fetch("https://api.paddle.com/subscriptions", {
    headers: { Authorization: `Bearer ${process.env.PADDLE_API_KEY}` },
  })
  const data = await res.json()
  return Response.json(data.data)
}

Integrating Analytics Providers

Mixpanel

import mixpanel from "mixpanel-browser"

mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_TOKEN!)
mixpanel.track("Dashboard Viewed", { section: "revenue" })

Segment

import { AnalyticsBrowser } from "@segment/analytics-next"

const analytics = AnalyticsBrowser.load({
  writeKey: process.env.NEXT_PUBLIC_SEGMENT_WRITE_KEY!,
})

analytics.track("Report Generated", { format: "pdf" })

Removing Unused Features

Delete directories you don't need:

  • Don't need forecasting? Delete app/(dashboard)/forecasting/
  • Don't need benchmarks? Delete app/(dashboard)/benchmarks/
  • Don't need invoices? Delete app/(dashboard)/manage/invoices/
  • Don't need integrations? Delete app/(dashboard)/manage/integrations/
  • Don't need reports? Delete app/(dashboard)/manage/reports/
  • Don't need alerts? Delete app/(dashboard)/manage/alerts/
  • Run pnpm build to verify no broken imports
  • Remove corresponding sidebar items from data/seed.ts

Using shadcn/ui CLI

Add new components alongside the kit:

npx shadcn@latest add <component-name>

Components install to components/ui/ and integrate seamlessly with the existing design tokens. The kit uses the new-york style with CSS variables enabled.