Skip to main content
Back to Blog
Tutorials
4 min read
December 5, 2024

How to Build a Notification Preferences Page in React

Create a notification preferences page with channel-based toggles, frequency settings, and category management in React.

Ryel Banfield

Founder & Lead Developer

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.

notificationspreferencessettingsReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles