Notification Content and Attention
Make a timely, relevant message understandable without overstating its outcome, exposing unnecessary detail or taking control of the current task.
Decide whether a notification is warranted
Start with the person’s current task, the event’s significance and an available next step. A message is useful when it provides needed information at an appropriate time; a product’s ability to generate messages is not a reason to interrupt everyone. Separate requested reminders and operational results from promotion.
Identify the actual recipient, record, revision and purpose. Relevance must come from those relationships or an explicit preference, not a name pasted into generic copy. Do not broaden a language/source notification to people who have not requested it.
| Question | Decision to record |
|---|---|
| Why this person? | Their task, subscription or explicit request; not an arbitrary audience |
| Why now? | The verified event and timing; not merely an available sending slot |
| What remains true? | Their retained input, source context and current permissions |
| What can they do? | A reachable supported action or understandable informational outcome |
| Can it wait? | The consequence of delay and their interruption preference |
| What proves the result? | The relevant returned/persisted state, separate from the message itself |
Lead with important information and a real next step
Put the event or consequence early enough to survive skimming, a compact preview or truncation. Keep the action explicit: review a retained draft, retry a failed query or open the relevant record. A vague “Something happened” title followed by a promotional introduction hides the useful part.
Use the identity and context needed to distinguish the event without repeating product branding already supplied by the channel. A title, body and action have different jobs; avoid making each repeat the same long sentence. The next step must reach the actual record and current state, not a decorative button.
| Part | Job | English interface example |
|---|---|---|
| Title | Name the important event | Draft available to review |
| Body | Retained context and consequence | Your local example text is still here. |
| Action | Name the supported operation | Open local record |
| Recovery | Provide a path after failure or interruption | Retry while retaining the draft |
| Qualification | Bound the outcome | Queued locally; no remote transfer occurred |
Stable copy, dynamic fields and source confidence
Make the message’s key meaning predictable. Use dynamic fields only when they provide verified context and have a useful missing-value behavior. A short title should not depend on a fragile combination of a person’s name, an unknown count and a translated sentence fragment.
Keep interface copy distinct from source content. Preserve source spelling, review qualifications and permission purpose. A parser finding a record or a local promise resolving is not sufficient evidence for a message claiming translation accuracy, qualified review or publication.
Capture the content/revision needed by the message. Later edits should not silently rewrite a past event or make its action point at an unrelated result. Prefer an addressable record with current-state reconciliation when opened.
Working content and attention lab
Capture a real local notice record from the editor. Changes to the editor do not rewrite captured records. Compare retained history, queued delivery and live-region text; they are separate states. Open a record to inspect its exact captured content, then close and return to the trigger.
The attention settings are explicit local preferences. They do not authenticate a user or grant real notification consent. Redaction controls compact previews and new live messages in this public fixture; it cannot protect a payload already present in the page. No SMS, push or email is sent.
This English browser model retains local records and requests live-region output. It sends no push, SMS or email and does not prove that a person heard, read or acted on a notice. All example text remains public in this page; redacting a preview is not access control.
Capture requires a nonempty title and body. No wording, relevance, urgency or cultural authority is automatically approved.
Review target: 1–2000 grapheme clusters when available. It is not a provider limit or SMS segment estimate. Over-target content remains editable and capturable.
Text measurements appear when the controls are ready.
0 local records; 0 queued; 0 not opened in this example.
Changes to preferences do not retroactively release a queue or alter captured content. Turn deferral off and explicitly release queued notices. Disabling announcements retains new records in history, including time-sensitive ones.
Requested live output
Only one channel receives each update. The record’s placement means text was set in a live region; actual spoken output depends on the browser and assistive technology.
Local notice history
No local notices captured.
Characters, graphemes and compact previews
A character target is a review aid, not a semantic quality score. Put essential meaning before a compact boundary, preserve the full message in a suitable destination, and test actual clipping rather than assuming a fixed number fits every device or font.
JavaScript string length counts UTF-16 code units. Code-point count and user-perceived grapheme clusters can differ, especially for combining marks and joined emoji. The tool displays the measures separately and labels a missing grapheme segmenter rather than pretending the fallback is equivalent.
Measurement cannot decide whether a notification is relevant, accurate, actionable or respectful. Longer wording can be necessary to preserve a source qualification; reorganize the message and destination instead of deleting that meaning to satisfy an arbitrary target.
- Check long names, expanded text, translated interface strings and the actual preview surface.
- Keep full content available after opening the notification’s destination.
- Do not truncate the only permission, uncertainty or source qualification.
- Record whether a limit is a product review target, actual renderer limit or transport constraint.
Reference compact targets and actual limits
These captured values are editorial targets, not universal HTML, OS or provider limits. The local editor’s default 80-unit review budget measures its actual title/newline/body string and labels code units, code points and graphemes separately. It does not emulate all notification surfaces or certify SMS segments.
Dynamic title fields should stay short, have a useful missing/overlong fallback and be tested in translation. Put expandable detail in the body when the channel permits it; a clipped title is not guaranteed to expand. Keep onboarding-related notifications out of the middle of that onboarding task.
| Captured Material target | Scope | Implementation decision |
|---|---|---|
| Title below 29 characters | Compact notification writing recommendation | Front-load the event; confirm the actual renderer and destination. |
| Collapsed body below 40 characters | Compact preview recommendation | Retain full meaning in the expanded view or linked record. |
| Expanded body below 80 characters | Build from the compact explanation | Do not erase qualifications to meet the target. |
| One or two actions, one or two words each | Suggested compact action set | Choose supported operations and expose any remaining recovery elsewhere. |
| SMS below 160 Latin / 134 non-Latin characters | Captured writing shorthand, not an encoding/segmentation specification | Validate the provider encoding and final payload; do not classify language names to estimate delivery or cost. |
SMS and other transport boundaries
SMS delivery has provider/encoding/segmentation constraints beyond a browser character counter. A joined emoji or accented text can change the representation, while added links or prefixes can change the final payload. Validate the exact composed message and delivery contract with the chosen provider before claiming segment count, cost or successful delivery.
The local tool does not send or estimate billable SMS segments. It measures the title/body string, including their newline, before transport-specific additions. Link the recipient’s purpose, opt-out mechanism and delivery record to a real sending integration rather than treating an in-app preview as a message receipt.
- Distinguish prepared, submitted, provider-accepted, delivered and opened states.
- Keep idempotency, retries and recipient preferences in the actual sending system.
- A fallback channel requires its own permission and purpose; email or SMS is not automatically authorized because a push channel failed.
- Make linked destinations understandable and usable; a short link alone is not meaningful action copy.
Emoji, delight and tone
Use a calm, specific tone that fits the consequence. Celebratory wording does not belong in an unresolved failure, sensitive permission decision or uncertain review result. Avoid jokes or personalization that changes the perceived authority of the message.
Emoji can be ambiguous, inconsistently rendered or verbose when read by assistive technology. Do not make an emoji the only urgency, success, failure or action cue. The tool can expose its text-unit differences, but it cannot approve its social or cultural meaning.
- Keep the task outcome understandable as ordinary text.
- Avoid punctuation, capitalization or emoji repetition as the only emphasis.
- Review tone with the responsible audience/context, not merely a general style checker.
- Use delight sparingly after a verified appropriate outcome; never substitute it for evidence or recovery.
Dates, timing and interruption
Specify the day, relevant time and time zone when they change the next action. Relative wording can become stale when a message is delayed, grouped or reopened. Preserve the intended event time separately from when a notification was captured or delivered.
Avoid repeatedly interrupting an ongoing task. Batch related noncritical changes, retain a quiet queue and provide a deliberate way to inspect it. A time-sensitive exception should be a reasoned policy and an explicit preference where appropriate, not inferred from a red border or an alarming keyword.
The lab does not release old records merely because a preference changes. Its explicit queue release produces one summary instead of replaying every message. The record’s timestamp is its actual local capture time, not an appointment or proof of remote delivery.
Opting in, opting out and preference ownership
Explain the value and channel before asking for permission. Keep audience, frequency, purpose and interruption preferences understandable and reversible. An opt-out must affect the actual sender/queue policy, not merely hide one visible badge.
Keep notification preferences separate from source-record access, recording retention, research participation and publication permissions. A browser grant does not establish those other permissions. Neither an in-memory checkbox nor a local role picker is authentication.
In the lab, disabling announcements retains all new records without setting either live channel. Time-sensitive events cannot override that switch. Explicitly permitted time-sensitive notices can bypass deferral only while announcements remain enabled. Past captured content is not rewritten when preferences change.
Accessible feedback and persistent recovery
Use appropriate programmatic status semantics for updates that do not change context. Keep routine announcements polite and reserve urgent treatment for genuinely time-sensitive attention. Do not announce the same result from a live region, a focused summary and a toast simultaneously.
Arrival should not steal focus from typing or reading. Opening a record is a requested context change; move focus deliberately and restore it on close. A transient notification must not contain the only recovery action or essential source detail.
The lab keeps history outside a live log and sets only one of its two message channels per update. “Live-region” placement records a DOM operation; it does not prove what a screen reader spoke or whether the person understood it.
- Keep complete message context available independently of a timed visual notice.
- Check repeated events, grouped updates and cancellation/restoration without duplicate task effects.
- Verify keyboard focus, reading time, reduced motion, both themes and narrow placement.
- Use a named assistive-technology/browser pair before claiming spoken behavior.
Privacy and truthful preview design
Decide which content is appropriate in a compact or potentially exposed preview. Keep sensitive detail in an authorized destination and avoid revealing it through a supposedly generic live announcement. A preview can be redacted while the event identity and next step remain clear.
Real privacy requires the correct payload and access boundary. Hiding a title in CSS or changing a local preview does not protect data already delivered to an unauthorized client. The lab is expressly public and uses no private records.
Use the current source/review/permission state when opening a real notification. Do not turn an old queued approval event into permanent authority.
Model source
This helper preserves explicit author-assigned priority and local preference ownership. Its text counts have distinct units; it performs no semantic classification or automatic approval.
export interface NoticeMeasures {codeUnits:number;codePoints:number;graphemes:number|null}
export function measureNoticeText(text:string):NoticeMeasures {
let graphemes:number|null=null;
if(typeof Intl.Segmenter==='function') {
try{graphemes=Array.from(new Intl.Segmenter(undefined,{granularity:'grapheme'}).segment(text)).length;}catch{graphemes=null;}
}
return {codeUnits:text.length,codePoints:Array.from(text).length,graphemes};
}
export type NoticePriority='routine'|'time-sensitive';
export type NoticePlacement='history-only'|'queued'|'live-region';
export interface NoticeRecord {id:number;title:string;body:string;priority:NoticePriority;createdAt:string;placement:NoticePlacement;opened:boolean}
export interface NoticePreferences {enabled:boolean;defer:boolean;urgentMayInterrupt:boolean;redact:boolean}
export function choosePlacement(priority:NoticePriority,preferences:NoticePreferences):NoticePlacement {
if(!preferences.enabled)return 'history-only';
if(preferences.defer&&!(priority==='time-sensitive'&&preferences.urgentMayInterrupt))return 'queued';
return 'live-region';
}
export function noticePreview(record:NoticeRecord,redact:boolean) {
return redact?{title:'New local notice',body:'Open the local record for its full details.'}:{title:record.title,body:record.body};
}
Compiled implementation
The checked example below captures immutable scalar record fields, keeps history and message placement separate, batches a queued release and returns focus after explicit inspection. It does not use browser notification permissions or an external sender.
'use client';
import {useEffect,useRef,useState} from 'react';
import {Button} from '@mobtranslate/ui';
import {measureNoticeText,choosePlacement,noticePreview,type NoticePriority,type NoticeRecord,type NoticePreferences} from '../lib/notice-content';
export function NoticeContentExample() {
const [ready,setReady]=useState(false);
const [title,setTitle]=useState('Draft ready');
const [body,setBody]=useState('Review the local example before sharing.');
const [budget,setBudget]=useState('80');
const [priority,setPriority]=useState<NoticePriority>('routine');
const [preferences,setPreferences]=useState<NoticePreferences>({enabled:true,defer:false,urgentMayInterrupt:false,redact:true});
const [records,setRecords]=useState<NoticeRecord[]>([]);
const [polite,setPolite]=useState(''),[urgent,setUrgent]=useState('');
const [openedId,setOpenedId]=useState<number|null>(null);
const [openEpoch,setOpenEpoch]=useState(0);
const sequence=useRef(0),returnTarget=useRef<HTMLElement|null>(null),detail=useRef<HTMLHeadingElement>(null);
const captureTarget=useRef<HTMLElement>(null);
useEffect(()=>setReady(true),[]);
useEffect(()=>{if(openedId!==null)detail.current?.focus();},[openedId,openEpoch]);
const message=`${title}\n${body}`;
const measures=measureNoticeText(message);
const numericBudget=Number(budget),budgetValid=budget.trim()!==''&&Number.isInteger(numericBudget)&&numericBudget>=1&&numericBudget<=2000;
const count=measures.graphemes??measures.codePoints;
const valid=Boolean(title.trim()&&body.trim());
const latest=records[0],queued=records.filter(record=>record.placement==='queued');
const opened=records.find(record=>record.id===openedId);
const preview=latest?noticePreview(latest,preferences.redact):null;
function publish(record:NoticeRecord) {
const text=noticePreview(record,preferences.redact);
const announcement=`Local notice ${record.id}: ${text.title}\n${text.body}`;
if(record.priority==='time-sensitive'){setPolite('');setUrgent(announcement);}
else {setUrgent('');setPolite(announcement);}
}
function capture() {
if(!valid)return;
const record:NoticeRecord={id:++sequence.current,title,body,priority,createdAt:new Date().toISOString(),placement:choosePlacement(priority,preferences),opened:false};
setRecords(current=>[record,...current]);
if(record.placement==='live-region')publish(record);
}
function release() {
if(!preferences.enabled||preferences.defer||!queued.length)return;
const ids=new Set(queued.map(record=>record.id));
setRecords(current=>current.map(record=>ids.has(record.id)?{...record,placement:'live-region' as const}:record));
const text=queued.length===1?'1 queued local notice is now available. Open local history for details.':`${queued.length} queued local notices are now available. Open local history for details.`;
if(queued.some(record=>record.priority==='time-sensitive')){setPolite('');setUrgent(text);}else{setUrgent('');setPolite(text);}
if(valid)captureTarget.current?.focus();else document.getElementById('notice-title')?.focus();
}
function openRecord(id:number,trigger:HTMLElement) {
returnTarget.current=trigger;setOpenedId(id);setOpenEpoch(current=>current+1);setRecords(current=>current.map(record=>record.id===id?{...record,opened:true}:record));
}
return <div data-notice-content-ready={ready} className="space-y-4">
<p>This English browser model retains local records and requests live-region output. It sends no push, SMS or email and does not prove that a person heard, read or acted on a notice. All example text remains public in this page; redacting a preview is not access control.</p>
<label htmlFor="notice-title" className="block">Important information first: title</label>
<input id="notice-title" className="mt-input w-full" disabled={!ready} value={title} onChange={event=>setTitle(event.target.value)} aria-invalid={!title.trim()} aria-describedby="notice-editor-help" />
<label htmlFor="notice-body" className="block">Context and next action: body</label>
<textarea id="notice-body" className="mt-textarea w-full" disabled={!ready} value={body} onChange={event=>setBody(event.target.value)} aria-invalid={!body.trim()} aria-describedby="notice-editor-help" />
<p id="notice-editor-help">Capture requires a nonempty title and body. No wording, relevance, urgency or cultural authority is automatically approved.</p>
<div className="grid gap-4 sm:grid-cols-2">
<label htmlFor="notice-budget">Author review target<input id="notice-budget" type="number" min="1" max="2000" step="1" className="mt-input w-full mt-2" disabled={!ready} value={budget} onChange={event=>setBudget(event.target.value)} aria-invalid={!budgetValid} aria-describedby="notice-budget-help" /></label>
<label htmlFor="notice-priority">Author-assigned attention<select id="notice-priority" className="mt-input w-full mt-2" disabled={!ready} value={priority} onChange={event=>setPriority(event.target.value as NoticePriority)}><option value="routine">Routine</option><option value="time-sensitive">Time-sensitive</option></select></label>
</div>
<p id="notice-budget-help">Review target: 1–2000 grapheme clusters when available. It is not a provider limit or SMS segment estimate. Over-target content remains editable and capturable.</p>
{ready?<><dl className="grid gap-3 sm:grid-cols-3" data-notice-measures>
<div><dt>UTF-16 code units</dt><dd data-notice-units>{measures.codeUnits}</dd></div>
<div><dt>Unicode code points</dt><dd data-notice-points>{measures.codePoints}</dd></div>
<div><dt>Grapheme clusters</dt><dd data-notice-graphemes>{measures.graphemes??'Unavailable in this runtime'}</dd></div>
</dl>
<p data-notice-budget-result>{!budgetValid?'Enter a review target from 1 to 2000.':`${count} ${measures.graphemes===null?'code points (fallback)':'grapheme clusters'} including the title/body newline: ${count>numericBudget?'over':'within'} the author target of ${numericBudget}.`}</p></>:<p>Text measurements appear when the controls are ready.</p>}
<fieldset className="space-y-2"><legend className="font-semibold mb-2">Local attention and preview preferences</legend>
<label className="flex items-center gap-3 min-h-11"><input id="notice-enabled" type="checkbox" disabled={!ready} checked={preferences.enabled} onChange={event=>setPreferences(current=>({...current,enabled:event.target.checked}))} />Allow new notice announcements</label>
<label className="flex items-center gap-3 min-h-11"><input id="notice-defer" type="checkbox" disabled={!ready} checked={preferences.defer} onChange={event=>setPreferences(current=>({...current,defer:event.target.checked}))} />Defer while I am busy</label>
<label className="flex items-center gap-3 min-h-11"><input id="notice-interrupt" type="checkbox" disabled={!ready} checked={preferences.urgentMayInterrupt} onChange={event=>setPreferences(current=>({...current,urgentMayInterrupt:event.target.checked}))} />Allow time-sensitive notices to bypass deferral</label>
<label className="flex items-center gap-3 min-h-11"><input id="notice-redact" type="checkbox" disabled={!ready} checked={preferences.redact} onChange={event=>setPreferences(current=>({...current,redact:event.target.checked}))} />Hide title/body in compact previews and new announcements</label>
</fieldset>
<div className="flex flex-wrap gap-3"><Button ref={captureTarget} data-notice-capture type="button" disabled={!ready||!valid} onClick={capture}>Capture local notice</Button><Button data-notice-release type="button" variant="outline" disabled={!ready||!preferences.enabled||preferences.defer||!queued.length} onClick={release}>Release queued notices</Button></div>
<p role="status" aria-live="off" data-notice-counts>{records.length} local {records.length===1?'record':'records'}; {queued.length} queued; {records.filter(record=>!record.opened).length} not opened in this example.</p>
<p>Changes to preferences do not retroactively release a queue or alter captured content. Turn deferral off and explicitly release queued notices. Disabling announcements retains new records in history, including time-sensitive ones.</p>
<div><p className="font-semibold">Requested live output</p><p role="status" aria-atomic="true" data-notice-polite style={{whiteSpace:'pre-wrap',overflowWrap:'anywhere'}}>{polite}</p><p role="alert" aria-atomic="true" data-notice-urgent style={{whiteSpace:'pre-wrap',overflowWrap:'anywhere'}}>{urgent}</p><p className="text-sm">Only one channel receives each update. The record’s placement means text was set in a live region; actual spoken output depends on the browser and assistive technology.</p></div>
{preview&&latest&&<div data-notice-preview className="border border-[var(--color-muted-foreground)] rounded-md p-4">
<p className="font-semibold">Compact preview of local notice {latest.id}</p><p data-notice-preview-title className="font-semibold break-words">{preview.title}</p><p data-notice-preview-body className="break-words whitespace-pre-wrap">{preview.body}</p>
<Button type="button" variant="outline" onClick={event=>openRecord(latest.id,event.currentTarget)}>Open latest local record</Button>
</div>}
{opened&&<section aria-labelledby="notice-opened-title" data-notice-detail>
<h3 id="notice-opened-title" ref={detail} tabIndex={-1} className="font-semibold text-xl scroll-mt-24">Local notice {opened.id}</h3><p data-notice-detail-title className="font-semibold break-words">{opened.title}</p><p data-notice-detail-body className="break-words whitespace-pre-wrap">{opened.body}</p><p>Captured at <time dateTime={opened.createdAt}>{opened.createdAt}</time>. Opened is a local viewing event, not proof of comprehension or review.</p>
<Button type="button" variant="outline" onClick={()=>{setOpenedId(null);if(returnTarget.current?.isConnected)returnTarget.current.focus();else document.getElementById('notice-title')?.focus();}}>Close local record</Button>
</section>}
<section aria-labelledby="notice-history-title"><h3 id="notice-history-title" className="font-semibold text-xl">Local notice history</h3>
{!records.length?<p>No local notices captured.</p>:<ol data-notice-history className="space-y-4 mt-3">{records.map(record=><li key={record.id} data-notice-record={record.id} className="border-t border-[var(--color-border)] pt-3">
<p className="font-semibold break-words">Notice {record.id}: {record.title}</p><p>{record.priority} · {record.placement} · {record.opened?'opened in example':'not opened'}</p><Button type="button" variant="outline" onClick={event=>openRecord(record.id,event.currentTarget)}>Open local notice {record.id}</Button>
</li>)}</ol>}
</section>
</div>;
}
Verification and failure cases
- Capture a record, edit the form and verify the record/destination retain the original values.
- Defer several arrivals, change the preference, then release once; verify one summary and the retained records.
- Disable announcements and capture both priorities; verify neither live node changes while history advances.
- Exercise the time-sensitive deferral exception and confirm it never bypasses disabled announcements.
- Compare a joined emoji’s UTF-16, code-point and grapheme counts, and test the explicitly labelled unavailable-segmenter path.
- Check blank content, invalid review targets, over-target content, long text, redacted previews and keyboard open/close/reopen.
- For a real integration, verify sender permission, exact payload, idempotency, provider/delivery outcomes, opt-out, current record access and accessible task recovery.
- Do not treat a badge, promise callback, preview, captured event or requested live announcement as proof of publication, delivery, comprehension or qualified review.