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
- Create a new route in
app/(dashboard)/your-page/page.tsx - The page automatically inherits the dashboard layout (sidebar, header)
- Add a sidebar link in
data/seed.tsunder 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 buildto 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.