Take the icon off the snackbar and lift the FAB clear of it

M3 says to avoid an icon in a snackbar and gives the supporting text no fourth
colour, so the state glyph and the 80% description are gone: a type now only
picks the announcement role. The 40px action and close buttons take the shared
touch-target, Escape dismisses a snackbar that holds the focus, and the host
publishes --material-snackbar-height on <html> so a `fab` button sits above the
snackbar instead of under it. Plan step 18, actions.md ACT-17, ACT-18, ACT-20,
ACT-34, ACT-35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold
2026-09-14 06:09:47 +02:00
co-authored by Claude Opus 5
parent eac7f6a1de
commit 7bc4f740e7
7 changed files with 76 additions and 33 deletions
+33 -4
View File
@@ -35,13 +35,27 @@ document.addEventListener('alpine:init', () => {
timer: null,
remaining: 0,
startedAt: 0,
escape: null,
init() {
host = this
pending.splice(0).forEach((detail) => this.add(detail))
// M3: Esc dismisses the focused snackbar. Only the focused one — a key pressed
// anywhere else on the page belongs to whatever has the focus there.
this.escape = (event) => {
if (event.key === 'Escape' && this.current && this.$el.contains(document.activeElement)) {
this.dismiss()
}
}
document.addEventListener('keydown', this.escape)
},
destroy() {
document.removeEventListener('keydown', this.escape)
document.documentElement.style.removeProperty('--material-snackbar-height')
if (host === this) {
host = null
}
@@ -93,6 +107,7 @@ document.addEventListener('alpine:init', () => {
clearTimeout(this.timer)
this.timer = null
this.current = this.queue.shift() ?? this.sticky
this.measure()
if (this.current?.timeout) {
this.remaining = this.current.timeout
@@ -100,6 +115,24 @@ document.addEventListener('alpine:init', () => {
}
},
/**
* Publishes the snackbar's height on <html> as `--material-snackbar-height`, and clears it
* when nothing shows. M3 puts a snackbar above a FAB, never in front of or behind one, and
* the FAB is somewhere else in the page: a variable is the only thing the two share.
*/
measure() {
this.$nextTick(() => {
const snackbar = this.$el.querySelector('[data-toast]')
const root = document.documentElement.style
if (snackbar) {
root.setProperty('--material-snackbar-height', `${Math.round(snackbar.getBoundingClientRect().height)}px`)
} else {
root.removeProperty('--material-snackbar-height')
}
})
},
pause() {
if (!this.current?.timeout || !this.timer) {
return
@@ -142,9 +175,5 @@ document.addEventListener('alpine:init', () => {
window.dispatchEvent(new CustomEvent(action.event))
}
},
icon(type) {
return ['success', 'error', 'warning', 'info'].includes(type)
},
}))
})