Banners
See how banners bring a message or action into the work area. They stay below the tab bar and accept mouse input while leaving keyboard focus with your work.
This page uses a saved copy of the design system, last changed on . It may differ from the current design source.
Banner — the floating top notice
Banner shell — the 4th overlay family
After Modal / Popup / Toast, Banner is the fourth overlay concept. It floats at the top of a scope's content area (View / Workspace / Pane / Tab / Surface) and never covers the tab bar — full width minus 8px side margins, 8px top margin, no bottom margin, 8px radius, variable height. Unlike a Toast it consumes its own mouse and can carry actions; unlike a Popup it takes no keyboard focus and has no titlebar/drag. Fired only by direct user action — never from agent/IPC. The example below is the first use: a TUI mouse-capture notice.
Do reach for a Banner when there's an action to attach (Popup = independent feature, Banner = info + action, Toast = info only). If the message is short and action-less, use a Toast instead.
There is no Info/Warning/Error kind — each banner's id is its kind, and it styles its own leading glyph/severity. These tokens are only the shared shell.
Lifetime — TTL follows whether the banner has an action (2026-11-02)
A banner exists only while it is valid. While valid: a banner that offers no user action has a TTL and closes on its own; a banner that offers an action (an inline button or a ⋯ menu row) has no TTL and stays until the user picks the action or closes it. The × is not an action — every banner has one, so it never makes a banner persistent. A developer may make an exception for a specific banner id; the exception and its reason are written as a code comment at the banner's definition.
Host banners today: TTL — preset applied (6 s), shell integration missing (10 s). No TTL — mouse capture (⋯ rows), auto-attach refused (Remove mapping), attach size sync (Retry), HTML script notice (Allow), HTML remote content (Allow), HTML mirror size / aggregate (Allow · Continue).
Dismiss & TTL countdown — top-right, one slot
The top-right corner is an affordance column holding one dismiss slot (a banner that carries a ⋯ overflow trigger adds it to the left of that slot — see Banner more menu). A banner with an action (no TTL) keeps its × hidden until you hover the banner. An action-less banner (TTL) shows a seconds countdown there instead, flipping to the × on hover; at 0 it auto-dismisses. The countdown pauses while the banner is hovered or its scope is backgrounded, then resumes. Appear/dismiss is a 120ms alpha fade — the banner never moves.
Queue & stacking — one per scope, rest wait
A scope shows one banner at a time; others queue (max 5). Re-firing the shown id resets its countdown; a queued duplicate id is ignored; a full queue drops new banners. When banners from different scopes coexist, the higher scope (View > Workspace > Pane > Tab > Surface) sits in front at a higher z-index, and the lower one is dimmed to ~40% opacity behind it — only its overhang shows.
The interactive manager (spawn / queue / background-pause / dismiss-pops-next) lives in the kit specimen ui_kits/terminal/overlays/banner.html.
Shell integration missing — action-less, TTL 10 s (2026-11-02)
shell-integration-missing — surface scope, countdown in the dismiss slot
Raised on a terminal surface when command-completion hooks need OSC 133 and no prompt marks were seen. Title + body, no buttons, so under the lifetime rule it is a TTL banner: the top-right slot shows the seconds, flips to × on hover, and the card closes at 0. The countdown starts when the card is shown, pauses while it is hovered or its scope is backgrounded, resumes where it stopped; re-raising the same id while shown resets it to 10. Card, glyph, title and body are unchanged — only the top-right slot changes.
Why 10 s: the body is two sentences plus a docs path (~25 words in en, longer in ja) — about 7–8 s of reading at a normal pace, plus time to notice a card that appears over live output. The 2 s toast default is for one-line confirmations and does not apply. Hover pauses the count, so a user who needs longer already has it. No setting: a per-banner TTL setting would be the only one of its kind; if users ask, a single global banner TTL setting is the place for it.
The glyph shown here is the specimen's; keep whatever the implemented card uses — this request changes only the top-right slot.
Mouse capture — the first banner instance
Anatomy & states
The canonical use: a TUI app turns on mouse tracking (DECSET 1000/1002/1003), so drag-to-select and right-click stop working. The banner explains why and the bypass. It is persistent (no TTL — it doesn't time out), fires once per tracking session and only on a real user click (never from agent/IPC). The top-right × is hidden until you hover; dismissing it suppresses the banner for that session. Leading glyph = a mouse (open decision resolved: keep it, in --tasty-banner-icon-fg tone). Card height is variable — body wraps to 1–3 lines depending on locale.
The bypass keys (Shift+drag / Shift+Right-click) are the message — keep them in the body. No inline action buttons: the body stays text-only, and the two per-app opt-outs live behind the ⋯ trigger next to the × (see Banner more menu below).
Position & hit-zone — card consumes, body passes through
The banner floats at the top of the surface content area, below the tab bar (never over it), full width minus 8px side margins. Crucially, the card rect — not the whole scope — is the mouse-consume + hover zone. Clicks on the surface body below the card pass through to the capturing app, so mouse reporting keeps working everywhere except the card itself.
Banner more menu — per-app opt-outs behind ⋯
⋯ trigger — second slot in the affordance column
The mouse-capture banner's two per-app opt-outs are not inline buttons — they sit behind a ⋯ trigger in the banner's top-right affordance column, left of the × with a 4px gap. Both share the same reveal rule: hidden until the banner is hovered (this banner has no TTL, so no countdown competes for the slot), and the ⋯ additionally stays lit while its menu is open. The card's width and height are unchanged — only the body's right reserve grows from 28 to 56px, so a 1-line body still fits on one line.
New glyph more (icons/more.svg — three dots, horizontal, same 2px round-cap language as M12 17h.01 style glyphs). A unicode ⋯ was rejected: the system renders every affordance through the Icon component so it inherits currentColor and the 16px sm size.
Don't keep the trigger permanently visible. A persistent banner with a permanently drawn ⋯ reads as a second dismiss control and adds chrome to a notice that is already at the top of live output — hover is where every other banner affordance lives.
The menu — 2 rows, anchored under the trigger
Clicking ⋯ opens a headless menu 4px below the trigger, right-aligned to it (the trigger sits at the card's right edge, so left-aligning would overflow), at a higher z-order than the banner. It flips above the trigger when the viewport can't fit it below. Same anchored-popup contract as the Tools menu: no scrim, outside click · Esc dismiss, ↑↓ move, ↵ select. Selecting a row acts immediately and closes the menu — to run the other one, reopen ⋯.
Both rows write a permanent, per-program Settings entry (copy: see Row copy below): Don't show again for {app} → mouse_capture_banner_blacklist (capture stays delegated, only the notice is suppressed) · Let Tasty handle clicks in {app} → mouse_capture_blacklist (tasty takes clicks and drags back; the wheel still goes to the program). “Don't show again” also closes the banner it was opened from — suppressing the notice and leaving it on screen would contradict itself. “Let Tasty handle clicks” leaves the banner up (capture is already released; the user dismisses when done reading). Icons: bell for the notice row, mouse for the capture row.
While the menu is open the banner is treated as hover-equivalent — a TTL banner would keep its countdown paused, and the affordance column stays drawn even if the pointer has left the card.
Elastic width — the interpolated program name
Every row interpolates a program name of arbitrary length, and word order differs by locale (en: name last; ko/ja: name first). So a row's label is two parts: the fixed text (never truncates) and the program name in mono (same row ink as the fixed text: text-secondary at rest, text-primary on hover — 2026-10-08), which is the part that shrinks and ellipsises. The menu grows with the content between 200px and 288px (a wider band than the Tools menu's 160–240, since every row carries a program name) and the full name is available as the row's tooltip.
Do keep the program name as the mono part of the row — it is the one thing the user must verify before writing a permanent per-app setting, and mono marks it as a process name rather than prose.
Don't paint the “Let Tasty handle clicks” row in danger tone (last card above, shown for the record). Nothing is destroyed and nothing is lost: the setting is reversible from Settings › Terminal, and it returns the mouse to tasty. Danger in this system means delete — spending it here would make the two rows look like different classes of action when they are the same class at two strengths.
Row copy — en / ko / ja (2026-11-02)
Each row names the program and what changes for it, in words a user already has: “show again” for the notice, “clicks” for capture. “Again” / “다시” / “今後” marks the first row as lasting; the second row reads as a standing rule (“in vim, Tasty handles clicks”). The old “~에 대해” and “disable mouse capture” (which read as turning off a feature of the app) are gone. Each row = prefix + {app} (mono) + suffix; en uses the prefix only, ko/ja the suffix only (leading space in ja before the particle). Width band 200–288 is unchanged; with this copy no row wraps for a short name in any locale (the old ja wrap is retired, the wrap rule stays for long names and future copy).
Settings › Terminal › Mouse Capture and the user guide take the same verbs: list 1 Don't show the capture notice for these programs / 캡처 안내를 다시 보지 않을 프로그램 / キャプチャ通知を表示しないプログラム · list 2 Let Tasty handle clicks in these programs / 클릭을 Tasty가 처리할 프로그램 / クリックを Tasty で処理するプログラム, with the existing description (clicks and drags go to selection and the menu; the wheel is still sent).
Program name unknown — rows shown disabled (2026-11-03)
When the program name is unknown (remote attach mirror — no local PTY; before the first 1 Hz poll; OS lookup failed) the actions still exist but can't run, so they are shown disabled, not hidden. The ⋯ trigger is drawn exactly as with a known name (same hover reveal, lit while open, same 56 body reserve). The menu opens with a non-interactive reason line on top, a separator, then the same two rows disabled — worded with “this program” in place of the name. No “(unknown)” placeholder: a mono “(unknown)” would read as a process called that. Clicking a disabled row does nothing: nothing is saved, the menu stays open, the banner stays. Esc / outside click close as usual. Keyboard arrow navigation skips disabled rows (with both disabled, focus stays on the menu). If the name arrives while the menu is open the menu does not change; the next open shows the normal rows. On a remote mirror the name is always unknown, so this is the menu there — a note, no separate example.
Do keep the rows the user would see with a known name, disabled, so the menu teaches what exists; the reason line says why it can't run now, without a hover.
Don't hide the ⋯ or the rows when the name is unknown (2026-11-02 option a, withdrawn). Don't carry the reason in a tooltip on a disabled row — pointer-events none on the row blocks it, and a tooltip is invisible to anyone who doesn't hover.
Mouse-capture blacklist (Settings › Terminal)
Per-program capture opt-out — list editor
The companion settings (labels aligned with the ⋯ rows 2026-11-02): Don't show the capture notice for these programs (mouse_capture_banner_blacklist) and Let Tasty handle clicks in these programs (mouse_capture_blacklist) — process-name patterns where mouse capture is disabled, so clicks/drags are handled locally (select / tasty menu) instead of being sent to the app. Promoted from the old newline textarea to a list editor (rows + Add) — resolved open decision. Grouped with the Show mouse-capture hint switch since both govern the same feature. Default is an empty list. Remove uses × (it un-lists a program, it doesn't delete anything).
The wheel is always forwarded to the program even for blacklisted apps — only clicks/drags are intercepted. A blacklisted foreground app also suppresses the capture banner on that surface.
HTML script notice — the first inset banner (2026-09-28)
Placement — inset, scoped to one surface
The HTML page is a native WebView layered over tasty's GPU frame, so a floating banner would be hidden under it. Approach ⒜: tasty paints the banner and the surface's WebView rect shrinks by the banner's height. This is a formal inset placement in the banner system, for any scope whose content is a native layer: same shell, margin 8 on all four sides (--tasty-banner-inset-gap is the new bottom one), and the page starts below it. Floating stays the default everywhere else. Scope is the surface: the terminal beside it is untouched. Showing or clearing the banner resizes the page once; there is no animation on the rect.
Don't inject the notice into the document DOM (⒞): the page could hide or forge it, and its button can't run with scripts off. Don't build three native overlays (⒝) for one notice.
Banner states
Informational, not a warning: blocking is the default working as intended. Leading glyph lock in --tasty-html-script-banner-glyph (→ accent-info). One action, [Allow for this document], Secondary / Sm — a relaxation should not be the loudest thing on screen, and there is no second action (the global switch lives in Settings › Appearance › HTML). Title and body wrap and never clamp — the card grows (Tasty-owned copy, same rule as refusal / size sync / mouse capture, 2026-10-10 b12); the button never wraps. The scope ("this document", "until Tasty restarts") is in the body copy. After Allow the document reloads once: the action is replaced by a spinner + Reloading with scripts allowed, × is withheld, and the banner fades out (120ms) when the new load commits.
A button on the banner shell (2026-09-29)
The shell is --tasty-banner-bg = surface-raised, which is also the Secondary button's fill, and its edge (border-default) is the same ramp step. On a banner the button would be text only. Rule for every banner: a Secondary inside a banner shell (data-surface="banner") goes one ramp step up, fill --tasty-banner-button-bg (→ surface-hover) with edge --tasty-banner-button-border (→ border-frame). Hover is the normal Button overlay (8%) on top; the edge does not change on hover here. No new variant: the context decides. Disabled on a banner follows the disabled rule (neutral box, disabled ink). Plugin banners too (2026-10-06): the rule belongs to the banner, not to who paints it. The platform opens the banner context for the whole frame of a plugin banner's content, so a plugin author draws a plain Secondary and gets the banner box; the context never leaks to the same plugin's surfaces or popups.