<!-- https://getkonjo.com/design/patterns/notifications · source: docs/design/patterns/notifications.md -->

# Notifications and messages to the person

Push, in-app banners, toasts, and badges — everything the app says without being asked.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them.

> **Before you write a word of consequence copy**, read
> [product-facts.md](../product-facts.md). Who is alerted, what becomes permanent, who
> can read it afterwards and whether it can be undone are decided there — and where they
> are not, that is a blocker rather than something to phrase well.

Konjo is an app about a place people physically attend. Every notification competes with a
dojo that already texts its students, and the fastest way to be uninstalled is to be the app
that nags. **A dojo does not guilt people into attending, and neither does its software.**

## The four channels

| Channel | For | Dismissal |
|---|---|---|
| **Push** | Something happening away from the app that has a time cost if missed | The OS |
| **In-app banner** | Persistent state the screen must account for — offline, a pending action, a blocked feature | Only when the state resolves |
| **Toast** | Confirmation of something the person just did | Itself, after a few seconds |
| **Badge** | A count of things waiting | Reading them |

Using the wrong one is what makes an app feel noisy: a toast for something that needs a
decision, a banner for something transient, a push for something that could have waited for
the badge.

## Push

**The test: would a person be annoyed to have learned this an hour later?** If not, it is not
a push.

Send:

- Someone did something *to them* — coached their video, replied, promoted them.
- Something they signed up for is imminent, once.
- Something needs their action and has a deadline — a waiver before a test.

Never send:

- A streak reminder, a "we miss you", or anything that leverages loss aversion. This is a
  settled decision, not a preference — see [progress](progress.md).
- Marketing, feature announcements, or "did you know".
- Anything they will see anyway next time they open the app.
- More than one per event. If three people comment, that is one notification.

**Copy:** the subject leads, the app is invisible. *"Sensei Marcus coached your side kick"* —
not *"Konjo: You have new feedback!"*. No exclamation marks, no emoji, no first person from
the app. It is the same [voice](../voice/voice.md) as everywhere else, at half the length.

**Every push lands on the specific thing**, never the tab root. See
[navigation](navigation.md).

## In-app banners

A banner is for a state, and it stays until the state ends.

- **A banner sits with what it is about.** A banner describing the *screen's state* — offline,
  a pending action, a feature switched off — is full width at the **top of the content**, in
  flow, never floating and never over the header. A banner describing **what just happened to a
  control** — a submit that failed — sits with that control, which on a form means directly
  above the primary; see [forms](form.md). Same component, and the position is the difference
  between "this screen is in a state" and "your tap did not work".
- **The feedback fill with its matching 1px border**, `caption` text. Fill alone is ~1.02:1
  against the page and reads as a slightly different background.
- **One banner at a time.** Two stacked banners is a screen with a problem the banners are not
  going to solve.
- **A banner offering an action offers exactly one**, as a text link.
- [`ValidationBanner`](../../../src/components/ui/ValidationBanner.tsx) already exists —
  extend it rather than writing another.

## Toasts

- **Confirmation only, for something the person just did.** "Session logged." Past tense,
  plain, naming what happened.
- **Never a toast for an error that needs a decision** — that is a banner or an inline error,
  because a toast disappears and a decision does not.
- **Never a toast for something the screen already shows.** If the row appeared, the row *is*
  the confirmation.
- **Undo lives in the toast** when the action is reversible, and the toast waits longer when
  it carries one.
- Use [`ToastProvider` / `useToast`](../../../src/components/ui/Toast.tsx). Do not build a
  second one.
- Announce it: `accessibilityLiveRegion="polite"`, or it does not exist for a screen-reader
  user.

## Badges

- **Count things a person can act on**, not things that merely happened.
- **Caps at 9+ in chrome**, 99+ elsewhere — see [content format](../foundations/content-format.md).
- **Reading clears it.** A badge that survives being read is a badge people learn to ignore,
  and it takes every other badge with it.
- The tab bar's badge is chrome and is exempt from the red budget — it is the one place a red
  dot is not spending the screen's red role.

## Preferences

- **Every push category is individually switchable**, and the settings screen uses the same
  words the notifications do.
- Turning a category off turns it off. There is no "important" override.
- `notification_preferences` and `push_token` are sensitive columns and stay out of the broad
  `users` select.

## Never

- A streak or attendance guilt notification.
- A toast carrying an error that needs a decision.
- Two banners on one screen.
- A badge that does not clear on read.
- Push copy that leads with the app's name.
- A notification that lands on a tab root instead of the thing it is about.
