Notification Preferences — Channel Toggle Matrix UI

Notification Preferences · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Event × channel matrix
A grid of event rows and Email/Push/SMS columns lets users set the whole picture at a glance.
Dirty-state save gate
Save enables only when preferences differ from the saved snapshot — toggling back disables it again.
Exact change tracking
A JSON snapshot comparison makes the dirty check precise, preventing pointless empty saves.
Three-state status line
Saved (gray), unsaved (amber), and just-saved (green) keep the user informed about their changes.
ARIA switch toggles
Each toggle is role="switch" with aria-checked and a label naming the event and channel for screen readers.
Sliding pill switches
Familiar track-and-thumb toggles with a transform-animated thumb — clear and GPU-cheap.
Data-driven matrix
Renders from an EVENTS array with per-channel prefs — add an event or channel with a data edit.
Sensible defaults
Critical notifications default on, promotional ones off — the defaults most users keep.

About this UI Snippet

Notification Preferences — Per-Event Email / Push / SMS Toggle Matrix

Screenshot of the Notification Preferences snippet rendered live

Every app with notifications eventually needs a preferences screen where users control *what* they're notified about and *how* — a grid of event types crossed with delivery channels (email, push, SMS). Done as a clear matrix, it lets people opt out of noise without turning everything off, which keeps the important notifications welcome. This snippet builds that preference matrix in plain HTML, CSS, and vanilla JavaScript: per-event toggles for each channel, a save gate that activates only when something changes, and accessible switch semantics.

A matrix, not a flat list

The layout is a grid: each row is an event type (Mentions, Comments, Security alerts, Billing…), and three columns are the delivery channels (Email, Push, SMS), each cell a toggle. This matrix is far clearer than a flat list of "Email me about X" checkboxes, because it lets the user see and set the whole picture at once — "I want security alerts on every channel but product updates on none" is two glances and a few taps. The column headers carry channel icons so the grid reads quickly even on a narrow screen.

Dirty-state save gate

The Save button stays disabled until the user actually changes something, and a status line reflects the state: "All changes saved" → "Unsaved changes" (amber) the moment a toggle differs from the saved snapshot → "✓ Saved" (green) after saving. This is implemented by comparing a JSON snapshot of the current preferences against the last-saved snapshot, so the dirty check is exact — toggling something and toggling it back correctly returns to "saved" with the button disabled again. This honest dirty-tracking is what prevents both pointless empty saves and the anxiety of not knowing whether your changes took.

Accessible toggle switches

Each toggle is a real <button> with role="switch" and aria-checked reflecting its state, plus an aria-label naming both the event and the channel ("Security alerts via SMS") — so a screen reader announces exactly what each of the eighteen toggles controls, which a grid of unlabeled switches completely fails to convey. The switch is styled as the familiar sliding pill (track + thumb) with a transform-animated thumb, so it's visually obvious and GPU-cheap.

Data-driven and snapshot-saved

The whole matrix renders from an EVENTS array, each event carrying a prefs object of channel → boolean. Adding an event type or a channel is a data edit. On save, the current preferences are captured as the new saved snapshot (and in a real app, sent to your backend) — the FAQs cover wiring that persistence. Because state lives in the data and the UI is its projection, the toggles, the dirty check, and the save all stay consistent automatically.

Sensible defaults matter

The starting preferences model good defaults — security and billing default to more channels (you want those), while product updates default to off (opt-in marketing) — because the defaults are what most users keep. A well-designed preferences screen still starts from defaults that respect attention: critical notifications on, promotional ones off, so the user only adjusts the exceptions. Swap the EVENTS and CHANNELS for your app's real notification types and delivery methods and it's ready.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the dirty-check by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why checkDirty() compares a full JSON snapshot of every event's prefs rather than tracking a simple "something changed" boolean, and what would go wrong with a naive boolean flag if a user toggled a switch and then toggled it right back. The same assistant can help optimize it, for instance asking whether re-stringifying the entire EVENTS array on every single toggle click would still be fast with dozens of event rows and multiple channels, or whether the aria-label string concatenation for each of the eighteen toggles could be generated more efficiently. It's also useful for extending the matrix: ask it to add a "select all for this channel" column-header toggle, group events into labeled sections (account, billing, marketing), or persist changes optimistically with a rollback if the save API call fails. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "notification preferences" matrix in plain HTML, CSS, and JavaScript — a grid of event types crossed with delivery channels, each cell a toggle — no form library.

Requirements:
- Render entirely from a data array where each event object has an id, a label, a sub-description, and a prefs object mapping channel names (e.g. email, push, sms) to booleans; render the grid so each event is a row and each channel is a column, generated dynamically from the data (not hardcoded per-row markup).
- Every toggle cell must be a real button with role="switch", an aria-checked attribute reflecting its current boolean, and an aria-label that names both the specific event and the specific channel it controls (not a generic label).
- Clicking a toggle must flip only that one event's one channel boolean in the underlying data model, then update that toggle's visual and ARIA state.
- Maintain a "saved" snapshot of the preferences taken at load time (and again after each save); after every toggle click, compare the full current state against that snapshot (not a simple changed-flag) to determine whether there are unsaved changes, and use that exact comparison to enable/disable a Save button and to display one of three status messages: "all changes saved" (nothing differs), "unsaved changes" (something differs), or a "saved" confirmation immediately after a successful save.
- Toggling a switch and then toggling it back to its original value must correctly return the UI to the "all changes saved" state with Save disabled again — the dirty check must be exact, not a one-way flag.
- Clicking Save must update the saved snapshot to the current state and show the "saved" confirmation status; leave a clear comment showing where a real API call would be made.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA notifications matrix renders with event rows and Email / Push / SMS toggle columns; Save is disabled.
  2. 2
    Toggle a preferenceTap any switch — the status changes to "Unsaved changes" (amber) and the Save button enables.
  3. 3
    Revert a changeToggle it back to the saved value — the status returns to "All changes saved" and Save disables again.
  4. 4
    SaveClick Save — the status shows "✓ Saved" and the current state becomes the new baseline.
  5. 5
    Edit events and channelsChange the EVENTS array (label, sub, prefs) and CHANNELS to match your app's notification types.
  6. 6
    Persist to your backendIn the save handler, send the EVENTS preferences to your API instead of only snapshotting locally.

Real-world uses

Common Use Cases

Account notification settings
The standard notifications preferences page in app settings — pair with integration cards and other settings sections.
Email subscription management
Let users fine-tune which emails they receive without fully unsubscribing.
Team and workspace alerts
Configure which workspace events notify members and through which channels, beside a profile dropdown in settings.
Mobile app push settings
Control push categories alongside email, with SMS for critical alerts.
Compliance and security alerts
Ensure security notifications are on across channels while letting marketing be opt-in.
Learning preference-matrix UX
A reference for a settings matrix, dirty-state gating, and ARIA switches — compare with a settings panel for general settings.

Got questions?

Frequently Asked Questions

In the save handler, send the EVENTS preferences (or a compact { eventId: { email, push, sms } } map) to your API via fetch, show a saving state, and on success update the saved snapshot to the new values so the dirty check resets. On load, fetch the user's saved preferences and initialize EVENTS from them, then snapshot that as the baseline so unmodified screens show "All changes saved".

It compares a JSON.stringify snapshot of the current preferences against the snapshot taken at the last save. Any difference means "dirty" (enable Save, show the amber status); identical means "clean" (disable Save). Because it's a full-state comparison, toggling a switch and toggling it back correctly returns to the clean state — unlike a naive "any change happened" flag that would stay dirty.

A toggle that visually slides on/off is semantically a switch, and role="switch" with aria-checked tells assistive tech to announce it as "on/off" rather than "checked/unchecked", matching its appearance and behavior. Pairing it with an aria-label that names both the event and the channel ("Billing via email") is essential here, since a bare switch in a grid gives a screen-reader user no context about what it controls.

Default critical and transactional notifications on (security alerts, billing, mentions) since users expect and need them, and default promotional or low-urgency ones off (product updates, marketing) as opt-in. Most users keep the defaults, so respecting their attention by not pre-enabling noise both improves the experience and aligns with consent best practices for marketing channels.

In React, hold the preferences in useState and compute the dirty flag by comparing to a saved ref, rendering toggles from the data; in Vue, use a reactive object with a computed dirty value; in Angular, use a component model with a getter. The toggle, snapshot-compare, and ARIA logic port unchanged — only the per-toggle re-render and the save API call move into the framework.