# Notification Notifications notify the user with the status of an action. Section: components Docs: https://nebula.kaluza.com/components/notification --- ## When to use - To inform the user about the result of an action they have taken - To inform the user about a system change - To draw the user's attention to something important, timely and relevant to their current task ## When not to use - **For static contextual information not tied to an action or system event** - use plain text instead - **For an absence of data** - use [Empty State](/components/empty-state) instead - **While data is loading** - use [Skeleton Loading](/components/skeleton-loading) instead - **For short statuses attached to objects** - use [Badge](/components/badge) instead - **More than once per page** - Notifications are disruptive. If overused, users will stop noticing them. If multiple are unavoidable, see Grouping below. ## Anatomy 1. **Icon** 2. **Title** 3. **Description** 4. **Actions** 5. **Close button** 6. **Error list** - only present in ErrorSummaryNotification ## Basic usage ```jsx

It may take up to 60 seconds to appear in the reads table once submitted

``` ## Variants and features ### Standard variants Notifications use colour and icons to convey the tone and priority of the information they contain. There are four variants: - **Info** - provide additional information relevant to the current context - **Success** - confirm an action was completed as expected - **Warning** - let the user know they are taking an action that is undesirable, or which might have unexpected results - **Error** - explain something has gone wrong, and what, if anything, the user can do to correct the problem The colours and icons are tied to each variant and should not be changed. Do not override the icon or create custom colour combinations - the icon ensures meaning is not communicated by colour alone. ```jsx <>

Meter reads take up to 60 seconds to appear in the reads table once submitted

Meter read submitted

Submitting this meter read will cause this customer to be re-billed

Meter read not submitted

``` ### Error summary `ErrorSummaryNotification` is a separate component used to summarise all validation errors on a form. Always render it on the page and pass errors to it when the form is submitted - it remains hidden until errors are received. With correct use, the following happens automatically: - Keyboard focus moves to the error summary when it appears - Each listed error links to its respective field ```jsx ``` In a real form, pass the errors when the form is submitted. The summary stays hidden until it receives them, then takes focus and links to the field: ```jsx const Example = () => { const [errors, setErrors] = useState([]); const onSubmit = (event) => { event.preventDefault(); setErrors([{ id: 'example', message: 'Enter a name' }]); } return ( <> Example form
Show errors
); } ``` ### Dismissal Notifications are optionally dismissible via the `dismissable` prop. Do not make a notification dismissible if it is critical for the user to read or interact with, or if they may need to refer back to it during their task. Use the `onDismiss` prop to perform an action when the notification is dismissed - for example, updating local storage. Consider whether the notification should be dismissed forever on this device, until the next session, or until the next page load. ```jsx console.log('Notification dismissed')} >

It can take 30 seconds or more to update the account

``` ### Single line For short notifications, title and description can be combined on a single line to save space. Omit the `title` prop and make the opening words of the description bold, separated from the remaining text by a non-bold hyphen. ```jsx

Meter read submitted - it may take 60 seconds to appear in the reads table

``` ### Actions Notifications can include up to two actions. Actions are buttons and should not be used for navigation - use a TextLink in the description instead. Do not use an action for dismissal; use the `dismissable` prop instead. ```jsx alert('Refetch action clicked'), }, ]}>

It may take up to 60 seconds to appear in the reads table once submitted

``` ## Grouping Use no more than one Notification per page where possible. On pages with multiple independent panels, limit to one per panel. If multiple notifications in the same place are unavoidable, stack them vertically in order of importance. Never place notifications side by side. ## Rules and guidance ### Usage - **Notification** WILL use the variant's predetermined colour tokens. There is no prop to change them. [id: notification.will-variant-colours; testability: auto] - **Notification** WILL render the icon tied to the current variant. The icon cannot be removed or overridden. [id: notification.will-variant-icon; testability: auto] - **Notification** MUST NOT be used to represent an absence of data. Rationale: Use EmptyState instead. An absence of data is not a status event. Using a notification here misrepresents what has happened. [id: notification.no-empty-state; testability: context] - **Notification** MUST NOT be used while data is loading. Rationale: Use Skeleton Loading instead. A notification during loading implies a status change has occurred, which is misleading. [id: notification.no-loading-state; testability: context] ### Placement - If there are validation errors on form submission: **ErrorSummaryNotification** MUST be placed at the top of the form. [id: notification.error-summary-placement; testability: context] - **Notification** SHOULD be placed close to the content it relates to. Rationale: Notifications that are physically distant from the content they describe are easy to miss and create a confusing relationship between the message and the thing it refers to. [id: notification.place-near-content; testability: manual] - If used for site-wide information such as upcoming maintenance: **Notification** SHOULD be placed at the very top of the page, above all headings. Rationale: Site-wide messages affect the entire page and must be seen before the user engages with any content. [id: notification.site-wide-placement; testability: context] - If conveying information the user needs to know before they start a form: **Notification** SHOULD be placed at the top of the form before any fields. Rationale: Users should have context before they begin entering data, not discover it partway through. Use sparingly. Consider whether a notification is necessary at all. [id: notification.form-pre-task-placement; testability: context] - If showing a conditional warning based on data the user has entered: **Notification** MAY be placed directly above the submit button. Rationale: Always consider whether the form design can be changed to avoid this scenario. [id: notification.form-conditional-warning-placement; testability: context] ### Content - **Notification error message** MUST tell the user what has gone wrong and, where possible, how to fix it. Rationale: An error message that describes the problem without offering a path forward leaves the user stuck. [id: notification.error-message.explain-and-resolve; testability: manual] - **ErrorSummaryNotification error message** MUST use the same wording as the inline field error for the same field. Rationale: Inconsistent wording between the summary and the field error creates confusion about whether they refer to the same problem. Exception: the inline error contains a link, in which case omit the link from the summary version. [id: error-summary-notification.error-message.match-inline-wording; testability: context] - **Notification title** MUST NOT end with a full stop or exclamation mark. [id: notification.title.no-terminal-punctuation; testability: auto] - If it is a single sentence: **Notification description** MUST NOT end with a full stop. [id: notification.description.no-terminal-full-stop; testability: auto] - **Notification error message** MUST NOT use vague or ambiguous language such as 'an error occurred' or 'invalid' without explanation. Rationale: Users need to understand what went wrong and what to do next. Vague messages leave them unable to act. [id: notification.error-message.no-vague-language; testability: manual] - **Notification error message** MUST NOT use whimsical language such as 'oops'. Rationale: Trivialises an error and undermines trust. [id: notification.error-message.no-whimsy; testability: manual] - **Notification error message** MUST NOT sound accusatory or make the user feel at fault. [id: notification.error-message.not-accusatory; testability: manual] - **Notification title** SHOULD be no more than 5 words. Rationale: Titles must be scannable. Users in task-focused contexts will not read long headings. [id: notification.title.max-five-words; testability: auto] - **Notification title** SHOULD be a statement, not a question. Rationale: Questions create ambiguity about what has happened. Statements are clearer and more direct. [id: notification.title.statement-not-question; testability: auto] - If the notification confirms a completed action: **Notification title** SHOULD use past tense to indicate completed actions. Rationale: Past tense ('submitted', 'sent') clearly signals completion. Present tense is ambiguous about whether the action is still in progress. [id: notification.title.past-tense-for-completed-actions; testability: manual] - **ErrorSummaryNotification title** SHOULD remain as 'There is a problem'. Rationale: Consistent error summary titles help users recognise the pattern across the platform. Exception: there is a strong reason to change it. [id: error-summary-notification.title.default-wording; testability: auto] - **Notification description** SHOULD NOT contain bold text. Rationale: Bold text in the description competes with the title and disrupts the visual hierarchy. The component already sets the hierarchy. Title renders bold, description renders plain. [id: notification.description.no-bold; testability: auto] - **Notification error message** SHOULD NOT use pleasantries such as 'please' or 'sorry'. Rationale: Adds length without adding meaning, and Nebula ships into client-branded apps whose voice may differ. See the content writing guide for Kaluza's default position. [id: notification.error-message.no-pleasantries; testability: manual] - **Notification title** SHOULD NOT use possessive adjectives such as 'your' or subjects such as 'the system' when context is clear. Rationale: These add length without adding meaning. [id: notification.title.no-possessive-or-subject; testability: manual] - **Notification title** SHOULD NOT use unnecessary qualifiers such as 'successfully' when the positive outcome is already implied. Rationale: Brevity aids scannability. If 'sent' already implies success, 'successfully sent' is redundant. [id: notification.title.no-unnecessary-qualifiers; testability: manual] - **Notification description** SHOULD NOT repeat or paraphrase the title. Rationale: The description should add information, not restate it. Repetition wastes the user's time and dilutes both messages. [id: notification.description.no-title-repetition; testability: manual] - **Notification description** SHOULD NOT contain questions. Rationale: Questions in notifications are confusing. The user does not know if they are expected to answer, or if it is rhetorical. [id: notification.description.no-questions; testability: manual] ### Interaction - **Notification** WILL apply role=alert for the error variant and role=status for all other variants. This can be overridden with the role prop if there is a clear accessibility justification. [id: notification.will-role-by-variant; testability: auto] - **ErrorSummaryNotification** WILL move keyboard focus to itself when the errors array becomes non-empty. The consumer does not need to call focus manually. [id: notification.will-error-summary-autofocus; testability: auto] - **Notification** WILL be removed from the DOM when dismissed. The parent must remount it to make it reappear. [id: notification.will-unmount-on-dismiss; testability: auto] - **Notification** MUST always be rendered in the DOM even when it has no content. Rationale: Screen readers need to anticipate where notifications will appear. The component renders nothing visually when empty, but its presence in the DOM means announcements are heard in the right place. [id: notification.always-in-dom; testability: auto] - **Notification** MUST NOT be made dismissible if the user needs to read or interact with it to complete their task, or may need to refer back to it. Rationale: Once dismissed the notification is removed from the DOM. If the user needs it again, they have no way to retrieve it. [id: notification.no-dismissal-if-critical; testability: manual] - **Notification actions** MUST NOT be used for dismissal. Rationale: Use the dismissable prop instead. Overloading an action button with dismissal creates an inconsistent pattern. [id: notification.actions.no-dismissal; testability: manual] - **Notification actions** MUST NOT be used for navigation to another page. Rationale: Actions are buttons. Use a TextLink inside the notification description instead. [id: notification.actions.no-navigation; testability: context] - **Notification** SHOULD NOT have more than two actions. Rationale: More than two actions crowds the notification and obscures the primary action. The component does not enforce this limit in code. [id: notification.max-two-actions; testability: auto] - **Notification** SHOULD NOT receive programmatic focus when dynamically revealed. Rationale: The live region (role=status or role=alert) announces the message to screen readers without focus moving. Moving focus interrupts the user's current context. Cases where focus movement is needed. Such as an error summary after form submission. Are handled automatically by ErrorSummaryNotification and Form, not by the base Notification. Exception: the notification requires the user to read or act before they can continue. [id: notification.focus-on-reveal; testability: context] ### Grouping - **Notification** MUST NOT be placed side-by-side with another Notification. Rationale: Side-by-side notifications compete visually and make it harder for users. Particularly screen reader users. To understand the relationship and priority of each message. Exception: used in a do and don't list. [id: notification.no-side-by-side; testability: manual] - **Notification** SHOULD NOT appear more than once on a page. Rationale: Multiple notifications compete for attention and reduce the impact of each. Overuse leads to alert fatigue. Exception: the page contains multiple independent panels, in which case no more than one Notification per panel. [id: notification.max-one-per-page; testability: auto] ## Accessibility ### In brief Notification is a message box that announces itself to a screen reader. An error is announced as an alert and every other variant as a status, and the dismiss button and actions are real buttons. ### What Nebula provides - The wrapper has the role `alert` for the error variant and `status` for the others, so new content is announced without moving focus. - The wrapper can take focus from code through its `ref`, and shows a focus ring when it does. - The icon is hidden from screen readers, so the variant is never announced by the icon alone. - The dismiss button and each action are `button` elements with a visible focus ring. The dismiss button is named "Dismiss". ### Keyboard behaviour The notification itself is not a tab stop. The dismiss button and the actions are buttons, reached with `Tab`. #### Focus order 1. The dismiss button, when the notification is dismissible 2. The actions, in the order they are passed #### Interactions - **Tab** - Moves focus to the dismiss button, then to each action in turn - **Enter or Space** - Dismisses the notification or runs the focused action ### Accessible design #### Put the meaning in the text, not in the colour or the icon The icon is hidden from screen readers and the colour is visual only, so a screen reader user hears the title and description and nothing else. Write them so they say what happened without the variant, for example "Meter read not submitted" and not just "Not submitted". WCAG: [1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) #### Avoid more than one dismissible notification in view Every dismiss button is named "Dismiss", so a screen reader user cannot tell which message each one closes. Keep to one dismissible notification in the same place. ### Accessible development #### Always render the Notification before its content arrives A live region is only announced when its content changes while it is already on the page. Render the Notification empty and pass the `title` or `children` when the message is ready: an empty Notification renders nothing visible but keeps its role. WCAG: [4.1.3 Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) #### If an error follows a form submit, move focus to the notification Focus the notification through its `ref` so the user can read the error and act on it. For every other message, leave focus where it is: the role already announces it, and moving focus scrolls the page and shows a focus ring the user did not ask for. `ErrorSummaryNotification` moves focus to itself when errors arrive, so do not focus it again. WCAG: [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html) #### Avoid overriding the `role` The defaults match the urgency of the message. An `alert` interrupts whatever the screen reader is saying, so do not set it on a message that is not an error. WCAG: [4.1.3 Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) ### Accessibility testing status Reviewed on 9 October 2026 against v17.0.1. | Test | Status | Tested with | | --- | --- | --- | | Default state | Tested | axe-core via the Storybook test runner (CI) | | Advanced states | Partially tested | axe-core via the Storybook test runner (CI) | | Keyboard navigation | Partially tested | keyboard (reported by the owner) | | Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) | What each test covers: - Default state: The component as it first appears, before anything is clicked or changed. - Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering. - Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring. - Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state. What these statuses mean: - Tested: Every check in this row passed in an automated run that repeats on every pull request. - Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change. - Partially tested: Some checks in this row were run once and passed; the rest are still to do. - Issue found: A check in this row failed. The ticket is shown beside the status. - Not tested: No check in this row has been run yet. - Not applicable: This row does not apply to the component, for the reason given beside it. The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used. ## Properties ### Notification Also accepts all props from `HTMLAttributes`. - `id` (type `string`, required). Set as the `id` of the notification's wrapper element. - `title` (type `ReactNode`, optional). Short bold text shown above the content. The notification renders nothing if it has neither a `title` nor children. - `noMargin` (type `boolean`, optional). Removes the default margin below the notification. - `variant` (type `'error' | 'success' | 'info' | 'warning'`, optional, default `'info'`). The kind of message. Sets the colour and icon, and the default ARIA role (`alert` for `error`, `status` for the rest). - `dismissable` (type `boolean`, optional). Shows a dismiss button. Clicking it removes the notification from the page without any state needed from the parent. - `onDismiss` (type `() => void`, optional). Called when the user dismisses the notification. - `actions` (type `Action[]`, optional). Follow-up actions shown as buttons below the content. Only shown when the notification also has a `title` or children. ### ErrorSummaryNotification Also accepts all props from `HTMLAttributes`. - `id` (type `string`, required). Set as the `id` of the notification's wrapper element. - `title` (type `ReactNode`, optional, default `<>There is a problem`). Short bold text shown above the content. The notification renders nothing if it has neither a `title` nor children. - `noMargin` (type `boolean`, optional). Removes the default margin below the notification. - `dismissable` (type `boolean`, optional). Shows a dismiss button. Clicking it removes the notification from the page without any state needed from the parent. - `onDismiss` (type `() => void`, optional). Called when the user dismisses the notification. - `actions` (type `Action[]`, optional). Follow-up actions shown as buttons below the content. Only shown when the notification also has a `title` or children. - `errors` (type `ErrorSummaryList`, required). The errors to list. Each `message` is shown as a link to the element whose `id` matches the error's `id` (usually the invalid field). While the list is empty the notification renders nothing; when errors arrive it appears with the `title` (`There is a problem` unless set) and takes focus. Keep the same array between renders unless the errors change - a new array moves focus to the notification again. ## Styles Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS. ### margin-bottom ```css @when !noMargin { margin-bottom: ${space[6]}; } ``` ### focus-wrapper ```css &:focus-visible { ${focusRing} } border-radius: ${radii.standard}; ``` ### notification-content ```css display: flex; flex-direction: column; gap: ${space[1]}; font-weight: ${fontWeights.normal}; font-family: ${fonts.body}; font-size: ${responsiveFontSizes.body[0]}; color: ${colors.body}; ${mediaQueries.smallAndUp} { font-size: ${responsiveFontSizes.body[1]}; } ``` ### icon ```css flex-shrink: 0; font-size: ${space[5]}; color: ${variants[variant].foreground}; ``` ### line-indent ```css position: absolute; top: -${borderWidths.standard}; bottom: -${borderWidths.standard}; left: -${borderWidths.standard}; width: ${borderWidths.heavier}; border-radius: ${radii.standard} 0 0 ${radii.standard}; background-color: ${variants[variant].foreground}; ``` ### notification-box ```css display: flex; gap: ${space[2]}; border: ${borderWidths.standard} solid ${currentVariant.foreground}; background-color: ${currentVariant.background}; border-radius: ${radii.standard}; position: relative; padding: ${space[2]}; padding-left: ${space[2] + borderWidths.heavier}; padding-right: ${dismissable ? numberToRem(remToNumber(space[2]) + remToNumber(space[10])) : space[2]}; ``` ### dismiss-button ```css background: none; border: none; padding: 0; width: ${space[10]}; height: ${space[10]}; position: absolute; color: ${colors.heading}; top: 0; right: 0; &:focus-visible { ${focusRing} } ``` ### error-list ```css &:first-child { margin-top: ${space[1]}; } &:last-child { margin-bottom: ${space[1]}; } ``` ### error-text-link ```css color: ${colors.error}; &:hover { color: ${colors.errorAlt}; } ```