Letting users control their notification settings improves trust and reduces churn. Here is how to build a comprehensive preferences page.
Step 1: Define Preference Types
// types/notifications.ts
export type NotificationChannel = "email" | "push" | "in_app";
export type NotificationFrequency = "instant" | "daily" | "weekly" | "never";
export interface NotificationCategory {
id: string;
name: string;
description: string;
channels: {
email: boolean;
push: boolean;
in_app: boolean;
};
}
export interface NotificationPreferences {
globalEnabled: boolean;
emailDigestFrequency: NotificationFrequency;
categories: NotificationCategory[];
quietHours: {
enabled: boolean;
start: string; // "22:00"
end: string; // "08:00"
};
}
export const defaultPreferences: NotificationPreferences = {
globalEnabled: true,
emailDigestFrequency: "daily",
categories: [
{
id: "security",
name: "Security Alerts",
description: "Login attempts, password changes, and security events",
channels: { email: true, push: true, in_app: true },
},
{
id: "updates",
name: "Product Updates",
description: "New features, improvements, and release notes",
channels: { email: true, push: false, in_app: true },
},
{
id: "comments",
name: "Comments & Mentions",
description: "When someone comments on or mentions you",
channels: { email: true, push: true, in_app: true },
},
{
id: "tasks",
name: "Task Assignments",
description: "When tasks are assigned to you or updated",
channels: { email: true, push: true, in_app: true },
},
{
id: "marketing",
name: "Tips & Tutorials",
description: "Helpful tips, best practices, and educational content",
channels: { email: true, push: false, in_app: false },
},
{
id: "billing",
name: "Billing & Invoices",
description: "Payment confirmations, invoices, and subscription changes",
channels: { email: true, push: false, in_app: true },
},
],
quietHours: {
enabled: false,
start: "22:00",
end: "08:00",
},
};
Step 2: Preferences Page
// components/notifications/NotificationPreferences.tsx
"use client";
import { useState, useEffect } from "react";
import type { NotificationPreferences, NotificationFrequency } from "@/types/notifications";
import { defaultPreferences } from "@/types/notifications";
export function NotificationPreferencesPage() {
const [prefs, setPrefs] = useState<NotificationPreferences>(defaultPreferences);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
fetch("/api/notifications/preferences")
.then((r) => r.json())
.then((data) => {
if (data.preferences) setPrefs(data.preferences);
});
}, []);
async function handleSave() {
setSaving(true);
setSaved(false);
await fetch("/api/notifications/preferences", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(prefs),
});
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
function toggleCategory(
categoryId: string,
channel: "email" | "push" | "in_app"
) {
setPrefs((prev) => ({
...prev,
categories: prev.categories.map((cat) =>
cat.id === categoryId
? { ...cat, channels: { ...cat.channels, [channel]: !cat.channels[channel] } }
: cat
),
}));
}
return (
<div className="mx-auto max-w-2xl space-y-8 py-10">
<div>
<h1 className="text-2xl font-bold">Notification Preferences</h1>
<p className="mt-1 text-gray-600">
Choose how and when you want to be notified.
</p>
</div>
{/* Global toggle */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div>
<p className="font-medium">Enable Notifications</p>
<p className="text-sm text-gray-500">
Turn off to mute all notifications
</p>
</div>
<Toggle
checked={prefs.globalEnabled}
onChange={(checked) =>
setPrefs((p) => ({ ...p, globalEnabled: checked }))
}
/>
</div>
{prefs.globalEnabled && (
<>
{/* Email digest frequency */}
<div className="space-y-3">
<h2 className="text-lg font-semibold">Email Digest</h2>
<p className="text-sm text-gray-500">
Receive a summary of notifications instead of individual emails.
</p>
<div className="flex gap-2">
{(["instant", "daily", "weekly", "never"] as NotificationFrequency[]).map(
(freq) => (
<button
key={freq}
onClick={() =>
setPrefs((p) => ({ ...p, emailDigestFrequency: freq }))
}
className={`rounded-lg border px-4 py-2 text-sm capitalize ${
prefs.emailDigestFrequency === freq
? "border-blue-600 bg-blue-50 text-blue-700"
: "hover:bg-gray-50"
}`}
>
{freq}
</button>
)
)}
</div>
</div>
{/* Category preferences */}
<div className="space-y-4">
<h2 className="text-lg font-semibold">Notification Categories</h2>
{/* Header */}
<div className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-4 text-xs font-medium uppercase text-gray-500">
<span>Category</span>
<span className="w-16 text-center">Email</span>
<span className="w-16 text-center">Push</span>
<span className="w-16 text-center">In-App</span>
</div>
{prefs.categories.map((category) => (
<div
key={category.id}
className="grid grid-cols-[1fr_auto_auto_auto] items-center gap-4 rounded-lg border p-4"
>
<div>
<p className="text-sm font-medium">{category.name}</p>
<p className="text-xs text-gray-500">{category.description}</p>
</div>
<div className="flex w-16 justify-center">
<Toggle
checked={category.channels.email}
onChange={() => toggleCategory(category.id, "email")}
size="sm"
/>
</div>
<div className="flex w-16 justify-center">
<Toggle
checked={category.channels.push}
onChange={() => toggleCategory(category.id, "push")}
size="sm"
/>
</div>
<div className="flex w-16 justify-center">
<Toggle
checked={category.channels.in_app}
onChange={() => toggleCategory(category.id, "in_app")}
size="sm"
/>
</div>
</div>
))}
</div>
{/* Quiet hours */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Quiet Hours</h2>
<p className="text-sm text-gray-500">
Pause push notifications during specific hours.
</p>
</div>
<Toggle
checked={prefs.quietHours.enabled}
onChange={(checked) =>
setPrefs((p) => ({
...p,
quietHours: { ...p.quietHours, enabled: checked },
}))
}
/>
</div>
{prefs.quietHours.enabled && (
<div className="flex items-center gap-3">
<label className="text-sm">
From
<input
type="time"
value={prefs.quietHours.start}
onChange={(e) =>
setPrefs((p) => ({
...p,
quietHours: { ...p.quietHours, start: e.target.value },
}))
}
className="ml-2 rounded-lg border px-2 py-1 text-sm"
/>
</label>
<label className="text-sm">
To
<input
type="time"
value={prefs.quietHours.end}
onChange={(e) =>
setPrefs((p) => ({
...p,
quietHours: { ...p.quietHours, end: e.target.value },
}))
}
className="ml-2 rounded-lg border px-2 py-1 text-sm"
/>
</label>
</div>
)}
</div>
</>
)}
{/* Save */}
<div className="flex items-center gap-3">
<button
onClick={handleSave}
disabled={saving}
className="rounded-lg bg-blue-600 px-6 py-2.5 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "Saving..." : "Save Preferences"}
</button>
{saved && (
<span className="text-sm text-green-600">Preferences saved</span>
)}
</div>
</div>
);
}
// Toggle component
function Toggle({
checked,
onChange,
size = "md",
}: {
checked: boolean;
onChange: (checked: boolean) => void;
size?: "sm" | "md";
}) {
const dimensions = size === "sm" ? "h-5 w-9" : "h-6 w-11";
const dotSize = size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
const translate = size === "sm" ? "translate-x-4" : "translate-x-5";
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative inline-flex shrink-0 cursor-pointer rounded-full transition ${dimensions} ${
checked ? "bg-blue-600" : "bg-gray-200"
}`}
>
<span
className={`pointer-events-none inline-block transform rounded-full bg-white shadow transition ${dotSize} ${
checked ? translate : "translate-x-1"
} translate-y-[3px]`}
/>
</button>
);
}
Step 3: Preferences API
// app/api/notifications/preferences/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { notificationPreferences } from "@/db/schema";
import { eq } from "drizzle-orm";
export async function GET() {
const userId = "current-user"; // Replace with auth
const [prefs] = await db
.select()
.from(notificationPreferences)
.where(eq(notificationPreferences.userId, userId));
return NextResponse.json({ preferences: prefs?.settings ?? null });
}
export async function PUT(request: NextRequest) {
const userId = "current-user"; // Replace with auth
const settings = await request.json();
await db
.insert(notificationPreferences)
.values({ userId, settings })
.onConflictDoUpdate({
target: notificationPreferences.userId,
set: { settings, updatedAt: new Date() },
});
return NextResponse.json({ success: true });
}
Need User Settings Interfaces?
We design and build settings pages, dashboards, and preference systems for SaaS applications. Contact us to build yours.