Skip to content

Notification preferences

The settings page every product ends up needing. Three notification categories (md-accordion) each hold an email/push/SMS matrix of md-switch controls with fieldset-style labeling, a md-multi-select picks the weekly digest topics, two md-time-pickers bound a quiet-hours window (overnight ranges supported), and a mute-all switch pauses everything at once — flagged by a warning md-chip and confirmed on save by a md-snackbar.

Live preview — toggle channels, try Mute all, then Save preferences
Notification preferences Choose how Meridian reaches you, per category and per channel.
Mute all notifications Pauses email, push and SMS until you turn it back on
Email Push SMS Feature announcements Tips and tutorials
Email Push SMS Invoices and receipts Payment failures
Email Push SMS New sign-ins Password changes (email always on)
Release notes Roadmap updates Community highlights Security advisories Pricing changes
Quiet hours Push and SMS are held overnight and delivered after the window ends.
Save preferences
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">

<!-- index.html — register the AWC UI elements once -->
<script type="module">
  import '@awc-ui/core/define';
</script>

<div style="display: flex; align-items: center; gap: 12px;">
  <span id="mute-label">Mute all notifications</span>
  <md-chip color="warning" appearance="filled" label="Notifications paused" hidden></md-chip>
  <md-switch aria-labelledby="mute-label" icons></md-switch>
</div>

<md-accordion heading-level="3" default-expanded="0">
  <md-accordion-item headline="Product" supporting-text="3 of 6 channels on" icon="rocket_launch">
    <div role="group" aria-label="Product notification channels">
      <!-- one grid row per notification type; every switch is named by
      its row label AND its column header -->
      <span id="p-em">Email</span> <span id="p-pu">Push</span> <span id="p-sm">SMS</span>
      <span id="p-feat">Feature announcements</span>
      <md-switch selected aria-labelledby="p-feat p-em"></md-switch>
      <md-switch selected aria-labelledby="p-feat p-pu"></md-switch>
      <md-switch aria-labelledby="p-feat p-sm"></md-switch>
    </div>
  </md-accordion-item>
  <!-- Billing and Security items follow the same shape -->
</md-accordion>

<md-multi-select label="Weekly digest topics" clearable
  supporting-text="Bundled into one summary email every Monday at 09:00">
  <md-select-option value="releases" selected>Release notes</md-select-option>
  <md-select-option value="advisories" selected>Security advisories</md-select-option>
  <md-select-option value="roadmap">Roadmap updates</md-select-option>
</md-multi-select>

<md-time-picker label="Start" name="quiet_start" format="24h" value="22:00" minute-step="15"></md-time-picker>
<md-time-picker label="End" name="quiet_end" format="24h" value="07:00" minute-step="15"></md-time-picker>

<md-button variant="filled">Save preferences</md-button>
<md-snackbar closeable></md-snackbar>

<script type="module">
  const mute = document.querySelector('[data-mute]');
  const muteChip = document.querySelector('[data-mute-chip]');
  const accordion = document.querySelector('md-accordion');
  const items = Array.from(document.querySelectorAll('md-accordion-item'));
  const channels = Array.from(document.querySelectorAll('md-switch[data-channel]'));
  const saveBtn = document.querySelector('[data-save]');
  const bar = document.querySelector('[data-saved-bar]');

  // Keep each category's supporting text in sync with its matrix.
  // md-switch's mdChange bubbles and is composed, so one listener on the
  // accordion hears every switch inside it.
  function refreshCounts() {
    items.forEach((item) => {
      const sws = Array.from(item.querySelectorAll('md-switch[data-channel]'));
      const on = sws.filter((s) => s.selected).length;
      item.supportingText = on + ' of ' + sws.length + ' channels on';
    });
  }
  accordion.addEventListener('mdChange', refreshCounts);
  refreshCounts();

  // Mute all: show the warning chip and disable every channel switch.
  // The always-on security email switch stays disabled either way.
  mute.addEventListener('mdChange', (e) => {
    const paused = e.detail.selected;
    muteChip.style.display = paused ? '' : 'none';
    channels.forEach((s) => {
      if (!s.hasAttribute('data-locked')) s.disabled = paused;
    });
  });

  // Save: confirm through the snackbar with a real summary.
  saveBtn.addEventListener('mdClick', () => {
    const active = channels.filter((s) => s.selected).length;
    bar.message = mute.selected
      ? 'Preferences saved — all notifications paused'
      : 'Preferences saved — ' + active + ' channels active';
    bar.show();
  });
</script>
ComponentRole in this screen
md-accordion + md-accordion-itemOne section per category. heading-level="3" slots real <h3> headings into the page outline; default-expanded="0" opens Product on load; each item’s supporting-text is a live “n of 6 channels on” summary, visible even while collapsed.
md-switchThe channel matrix. Each switch is named via aria-labelledby pointing at both its row label and its column header — the fieldset/legend pattern for a grid of toggles. The mute-all switch uses icons so its state reads by shape, not just color.
md-multi-selectDigest topics as removable chips. Options carrying selected are adopted at load, so the initial selection lives in markup, not script.
md-time-pickerThe quiet-hours window. format="24h" with minute-step="15" matches how delivery windows are actually scheduled; 22:00 → 07:00 is a legal overnight range.
md-chipThe color="warning" chip flags the muted state at the top of the screen — a theme role name that resolves to --md-sys-color-warning-* tokens.
md-snackbarSave confirmation with a channel count. Low-priority by design: it auto-hides after 4 s and never blocks the screen.
md-buttonThe save action, listening on mdClick.
  • Switch groups need fieldset-style labeling. md-switch has no default slot, so a bare switch has no accessible name. Here each matrix lives in a role="group" with an aria-label, and every switch’s aria-labelledby chains its row label and column header — a screen reader announces “Feature announcements Email, switch, on” instead of eighteen anonymous toggles.
  • Muting stays silent in the right way. Assigning selected or disabled from script never fires mdInput/mdChange on a switch — so bulk-disabling the matrix doesn’t trigger a cascade of phantom change handlers or corrupt the per-category counts.
  • “Always on” is disabled, not a lie. The password-changes email switch ships selected disabled: it renders as on, announces aria-disabled, and leaves the tab order, while the mute-all sweep explicitly skips it so unmuting never re-enables a channel the policy locked.
  • The quiet-hours window can cross midnight. md-time-picker treats a reversed min/max as an overnight range (like native <input type="time">), and value is always a canonical 24-hour HH:MM — what you submit is what you stored, regardless of display format.
  • The snackbar respects reading time. Its auto-hide timer pauses while the surface is hovered or focused (WCAG 2.2.1) and it announces through a polite live region, so the save confirmation is perceivable without stealing focus.
  • Settings-shell navigation: add exclusive keep-one-expanded to the accordion so exactly one category is open at a time — the locked-open panel advertises aria-disabled="true" on its header.
  • Server-confirmed SMS: make the SMS switches controlled — call preventDefault() on mdInput, verify the phone number server-side, then assign selected yourself once the carrier accepts.
  • Undo on save: give the snackbar action="Undo", call hide('action') from your mdAction handler, and branch on mdClose’s detail.reason to roll the preferences back.
  • Many digest topics: switch the multi-select to display-mode="count" with a countFormatter, or add filterable once the topic list passes ~15 entries.
  • Recipe: Two-factor verification — the security settings these notifications guard.
  • Component manuals: md-switch — the controlled-mode event split; md-accordion — heading levels, region roles and the full keyboard map.