Skip to content

[6.x] Wrangle toast messages - #19746

Open
brianjhanson wants to merge 9 commits into
6.xfrom
feature/wrangle-toast-messages
Open

brianjhanson wants to merge 9 commits into
6.xfrom
feature/wrangle-toast-messages

Conversation

@brianjhanson

Copy link
Copy Markdown
Contributor

The control panel had four separate ways of telling someone that something worked or failed: the legacy corner stack, a banner at the top of the page, text beside the Save button, and a client-side store. None of them knew about the others. Messages showed up twice, got lost between pages, or reappeared on unrelated pages, and screen reader support varied depending on which one you hit.

This PR replaces them with one queue and one display. Every message, whether it comes from a PHP controller, a JSON response, Vue, legacy JavaScript or a plugin, takes the same path, looks the same, and is shown once.

It's worth noting that this is ideally not the final state of these things. Toasts are generally not great and not recommended by accessibility experts. For the moment, I thought it was better to restore things to roughly how they worked in v5 vs. rethinking everything right now. With the plumbing in place, we should be able to adjust the UI down the line. I'm thinking we'll use the settings available on each message as a way to tell the system how to display the message (inline, banner, toast, etc.).

📖 Full documentation: docs/messages.md covers usage for core and plugins, inline messages, the display, accessibility and tuning.

What changes

  • One format. Messages have an id, a type (notice, success or error), text, display settings, and an optional inline target. The server collects them in a single list, and the browser never shows the same id twice.
  • Stored only when there's a next page. Redirects flash the message; JSON responses return it in the body. This stops stale messages turning up on later pages.
  • One display, modelled on Sonner.
    • A stack in the user's chosen corner that spreads out on hover or focus.
    • Errors, and messages with actions, stay until dismissed; everything else follows the user's duration setting.
    • Separate live regions for errors and for everything else.
    • A "Skip to messages" link, and swipe to dismiss.
    • It respects reduced motion and stays clear of Laravel Debugbar.
  • Inline messages when they belong beside a control, such as "Email sent" beside the Test button, with automatic fallback to the stack.
  • Existing APIs keep working. Craft.cp.display*(), Yii adapter setNotice()/setSuccessFlash(), and back()->with('success', …) all go through the new queue, so plugins don't need changes.

Upgrade notes

  • useFlash(), useFlashMessages() and FlashMessages are removed; use useMessages().
  • The flash Inertia prop is deprecated; use messages.
  • asSuccess() no longer flashes on JSON responses.
  • asFailure() keeps its summary message alongside validation errors.
  • The container is now , and the notifications--* body class is gone.

See the docs for details and examples.

Also included

  • Chips no longer reserve a blank thumbnail square for elements without a thumbnail.

brianjhanson and others added 7 commits September 29, 2026 15:04
Chips set `show-thumb` and emitted a thumbnail slot for every Thumbable
element, even when it had no thumbnail. Since the slot got a fixed size,
that showed up as a blank square. Only render the slot when there's
something to put in it; the reload settings still carry `showThumb`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flash::success()/error()/notice() now add `{id, type, message, settings,
target}` to a single `cp-messages` session list on control panel
requests (site requests keep the plain session keys). The Inertia
`messages` prop and the Twig `cpMessages()` function both read it via
Flash::all(), which also picks up messages flashed straight to the plain
keys. The old `flash` prop is kept but deprecated.

JSON responses from asSuccess()/asFailure() return their message in a
`messages` list (with the id also in `notificationSettings`) instead of
flashing it, so it can't reappear on a later page. asFailure() now keeps
its summary message when there are validation errors.

Controllers that used `->with('success'|'error')` now use Flash, and
FieldsController no longer flashes an error before a JSON validation
failure. Adds the `assertMessage()` test response macro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`resources/js/modules/messages` is the one path every control panel
message takes: server-flashed messages (Inertia `messages` prop, or
`window.CraftMessageQueue` on Twig pages), JSON responses
(`showMessagesFromResponse()`), the `craft-message` window event, and
legacy `Craft.cp.display*()` calls through a shim. `showMessage()` drops
repeats by id (remembered in sessionStorage), sends targeted messages to
inline outlets, and shows the rest in the default display.

The container is a new `<cp-messages>` element rendered by both the Twig
layout and `app.blade.php`. It builds two live regions up front
(`role="alert"` for errors, `role="status"` otherwise) and holds the
user's position preference in a `position` attribute, which Inertia
responses keep current.

`Craft.CP.Notification` still draws each message, now as a Sonner-style
stack: collapsed behind the newest, spread out on hover, press or focus,
at most five visible. Messages close after the user's notification
duration, except errors and ones with actions. Timers pause while the
stack is expanded or the tab is hidden. Adds swipe to dismiss, a
<craft-button> close button, reduced-motion support, a "Skip to
messages" skip link, and room for the Laravel Debugbar via the existing
`--cp-debug-bar-height`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the FlashMessages banner, useFlash() and the global
useFlashMessages() store (which leaked messages across pages and hid
client messages behind server ones) with useMessages(). The 30 client
calls now go through the shared queue, and the duplicate announce
watcher in PageScreen is gone.

InlineFlash becomes a named outlet: messages targeting it render beside
the control that caused them, with their own live regions, and fall back
to the default display when it isn't on the page. The email test and
Find and Replace use it; the page Save button no longer repeats the
page-level message. The login form shows OAuth errors targeted at
`login` inline instead of as a message. TransitionFade respects reduced
motion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds docs/messages.md: how messages travel, sending them from PHP
controllers, Flash, Vue and JavaScript, inline outlets, what plugins
should use (PHP, the Yii adapter, Craft.cp and the `craft-message`
event), the default display and its accessibility, setup, and tuning.
Replaces the module README, and adds changelog entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…messages

# Conflicts:
#	packages/craftcms-legacy/cp/src/js/CP.js
#	resources/js/modules/settings/composables/useSettingsSave.ts
#	src/Cp/Html/ElementHtml.php
#	src/Http/RespondsWithFlash.php
Renames `--messages-*` to `--cp-messages-*` and the per-message
`--message-index`/`--message-offset` to `--cp-message-*`, matching the
CP's other custom properties, and updates the documented defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

No changed components detected in this Storybook.

resources/js — open Storybook

No changed components detected in this Storybook.

…messages

# Conflicts:
#	CHANGELOG.md
#	resources/js/modules/elements/composables/useElementIndexPage.ts
@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

The merge from 6.x is committed as 2ee7a14 and not pushed. I couldn't run prettier or the type-check because pnpm isn't installed here. The code edit only swaps function calls, but CI should confirm it.

Blocked
Nothing.

Changed

  • CHANGELOG.md: Both sides had added lines under "Control Panel UI". I kept both: this branch's two message-system entries and 6.x's "element index searches update as users type" entry ([6.x] Element index refactors + additional feature parity with 5.x #19754). Nothing here was ambiguous.
  • resources/js/modules/elements/composables/useElementIndexPage.ts: This branch had moved this file off the old flash helpers onto useMessages(). Meanwhile, 72e2f3a on 6.x moved it to elements/index/composables/, and its structure-move code now lives in useElementStructureMoves.ts.
    • I accepted the deletion at the old path.
    • I made this branch's same edits in resources/js/modules/elements/index/composables/useElementStructureMoves.ts: useFlashMessages became useMessages, the two flash('error', …) calls became messages.error(…), and flash('success', …) became messages.success(…).
    • I left no CONFLICT-REVIEW comments. It's still worth a quick look, because this change lands in a file that wasn't itself in conflict.

Found
I searched for code coming in from 6.x that still uses the flash helpers, component, stylesheet or template this branch deleted, and found none.

@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

The merge had one conflict, in CHANGELOG.md, and it's now resolved and committed as fdb9685. I haven't pushed it.

In the "Control Panel UI" section of "Unreleased", both sides had added bullets at the same spot. This branch added two bullets about the new single system for control panel messages, and 6.x added two bullets from #19759 (inline editing in element index tables, and exports for element indexes). I kept all four: the 6.x entries come first, followed by this branch's two. Nothing was ambiguous, so I didn't leave any # CONFLICT-REVIEW: comments.

The other changed files in the merge merged without conflicts, and I didn't edit them.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants