Slack integration
Deployment notifications post to #operations.
Explicit notification markup, Rails flash mapping, and pauseable dismissal behavior.
app/components/nitro_kit/toast.rbNitroKit::Toast.new(id:, duration:) { |toast| toast.item(id:) }Every notification intent renders as explicit server-owned markup. The list is addressable as <toast id>-list so Turbo Streams can append items.
Default
Info
Success
Warning
Error
test/dummy/app/components/gallery/components/toast_page.rbturbo_frame_tag("gallery-toast-variants-frame") do
render NitroKit::Button.new(
"Replay dismissed notifications",
id: "gallery-toast-variants-replay",
href: "/gallery/components/toast",
size: :sm
)
render NitroKit::Toast.new(
duration: 600_000,
label: "Variant examples",
id: "gallery-toast-variants"
) do |toast|
NitroKit::Toast::Item::VARIANTS.each do |variant|
toast.item(
title: variant.to_s.humanize,
description: toast_description(variant),
variant:
)
end
end
endTitle, description, block content, permanent notices, and long messages are independent.
Workspace saved
Focus keeps this notification visible
Deployment details
Release 2026.07.13 is healthy in fra1 and iad1.
ProductionThe production deployment could not be promoted
test/dummy/app/components/gallery/components/toast_page.rbsample("Title only", slug: "title-only") do
render NitroKit::Toast.new(
duration: 600_000,
label: "Title only notification",
id: "gallery-toast-title-only"
) do |toast|
toast.item(title: "Workspace saved", id: "gallery-toast-workspace-saved")
end
end
sample("Permanent", slug: "permanent") do
render NitroKit::Toast.new(
label: "Permanent notification",
id: "gallery-toast-permanent"
) do |toast|
toast.item(
description: "A workspace owner must acknowledge this billing change.",
variant: :warning,
dismissible: false
)
end
end
sample("Timed pause", slug: "timed-pause") do
turbo_frame_tag("gallery-toast-timed-frame") do
render NitroKit::Button.new(
"Replay",
id: "gallery-toast-timed-replay",
href: "/gallery/components/toast",
size: :sm
)
render NitroKit::Toast.new(
duration: 1_200,
label: "Timed notification",
id: "gallery-toast-timed"
) do |toast|
toast.item(title: "Focus keeps this notification visible")
end
end
end
sample("Block content", slug: "block") do
render NitroKit::Toast.new(
duration: 600_000,
label: "Block notification",
id: "gallery-toast-block"
) do |toast|
toast.item(title: "Deployment details", variant: :info) do
p { "Release 2026.07.13 is healthy in fra1 and iad1." }
render NitroKit::Badge.new(
"Production",
id: "gallery-toast-environment",
color: :success,
size: :sm
)
end
end
end
sample("Long error", slug: "long-error") do
render NitroKit::Toast.new(
duration: 600_000,
label: "Long error notification",
id: "gallery-toast-long"
) do |toast|
toast.item(
title: "The production deployment could not be promoted",
description: "The release remains healthy in staging, but the primary database rejected the migration lock. Review the deployment log before retrying.",
variant: :error
)
end
endFlash rendering receives explicit data and never reaches through a template context.
test/dummy/app/components/gallery/components/toast_page.rbrender NitroKit::Toast::FlashMessages.new(
flash: {
notice: "Welcome back, Ada.",
success: "Production settings were saved.",
warning: "The payment method expires next month.",
alert: "Your session expired; sign in again."
},
duration: 600_000,
label: "Rails flash messages",
id: "gallery-toast-flash"
)Rails flash is the whole feedback contract. Render NitroKit::Toast::FlashMessages once in the application layout, set ordinary flash in the controller, and never add a client-side notification store for a server outcome.
A successful action redirects with status: :see_other (303) and flash[:success], which Turbo follows so the message renders on the next page. A failed action never redirects: it sets flash.now and re-renders with status: :unprocessable_entity (422), which Turbo renders in place. Severity keys map notice to the default presentation, alert and error to error, and success, warning, and info to their matching variants; an unknown key falls back to the default presentation.
Project created
Payment method was declined
test/dummy/app/components/gallery/components/toast_page.rbrender NitroKit::Toast::FlashMessages.new(flash: flash, duration: 5_000)
# app/controllers/projects_controller.rb#create, when the record saves.
# 303 so Turbo follows the redirect after a non-GET request; the flash
# survives it and renders on the next page.
redirect_to(@project, status: :see_other, flash: { success: "Project created" })
# The same action, when validation fails. No redirect, so flash.now, and
# 422 so Turbo renders the response in place instead of ignoring it.
flash.now[:alert] = "Project could not be created"
render(UI::Projects::New.new(@project), status: :unprocessable_entity)
# A Turbo Stream that neither redirects nor re-renders appends to the
# region's list, addressable as the toast id plus "-list".
turbo_stream.append("nk-toast-list") do
render NitroKit::Toast::Item.new(title: "Import finished", variant: :success)
endThe region is section[data-nk=toast] with role=region and the label passed as label:. Each notification is li[data-nk=toast-item] with aria-atomic=true and role=status, except the error variant, which uses role=alert so assistive technology interrupts. Every item is data-turbo-temporary so a cached page never replays old feedback, while the region survives and its ol stays addressable as the toast id plus "-list" for a Turbo Stream append. duration: is the auto-dismiss timer in milliseconds; it pauses on hover and focus, and dismissible: false keeps a notice on screen until the person dismisses the page.
Polite status
Assertive alert
test/dummy/app/components/gallery/components/toast_page.rbrender NitroKit::Toast.new(
duration: 600_000,
label: "Announcement examples",
id: "gallery-toast-announcement"
) do |toast|
toast.item(
title: "Polite status",
description: "role=status waits for a pause in speech.",
variant: :success
)
toast.item(
title: "Assertive alert",
description: "role=alert interrupts, so it is reserved for the error variant.",
variant: :error
)
endA realistic settings result keeps source data, action controls, and notifications explicit.
Deployment notifications post to #operations.
Slack settings saved
test/dummy/app/components/gallery/components/toast_page.rbrender NitroKit::Card.new(id: "gallery-toast-integration-card") do |card|
card.title("Slack integration", level: 3)
card.body do
render NitroKit::Badge.new(
"Connected",
id: "gallery-toast-integration-status",
color: :success
)
p { "Deployment notifications post to #operations." }
end
card.footer do
render NitroKit::Button.new(
"Configure",
id: "gallery-toast-configure",
href: "#configure",
variant: :default
)
end
end
render NitroKit::Toast.new(
duration: 600_000,
label: "Integration result",
id: "gallery-toast-integration-result"
) do |toast|
toast.item(
title: "Slack settings saved",
description: "New deployment notifications will use #operations.",
variant: :success
)
endConstructor options, rendered root, closed vocabularies, and compound boundary for this component, exactly as shipped.
docs/component_contracts.md · NitroKit::Toast
duration: 5000, label: defaulting to I18n.t("nitro_kit.toast.label"), id: "nk-toast"section[data-nk=toast][role=region] wrapping ol[data-slot=toast-list]; item variants default info success warning erroritem(title:, description:, variant:, dismissible:, id:) declarations inside its render block. Each item requires a title, description, or block. Items carry role="status", or role="alert" for the error variant, plus aria-atomic, so server-rendered flash items are announced without waiting for a DOM mutation; caller aria: colliding with the owned region label or item atomic raises. Every item is data-turbo-temporary, so a cached page never replays stale feedback; the region and list survive. The list id is the toast id plus -list, so a Turbo Stream can append to it: turbo_stream.append("nk-toast-list") { render NitroKit::Toast::Item.new(title: "Saved", variant: :success) }. Items with dismissible: false render no dismiss button and are never auto-dismissed; the application owns their removal. Toast::FlashMessages maps an enumerable Rails flash into items through explicit keywords. The dismiss control is named from nitro_kit.toast.dismiss.Application conventions this component belongs to. Each summary is the leading section of its pattern document.
docs/patterns/flash_and_toast.md
NitroKit::Toast::FlashMessages once in the application layout.flash.now when rendering the current request.Instructions for coding agents, repeated on every component page so one fetched page has enough context. Humans can usually skip this section.
test/dummy/app/components/gallery/agent_rules.rb
render NitroKit::Button.new("Save", variant: :primary).ArgumentError; this component's accepted options are in the contract above.html: for HTML, aria: for ARIA, and data: for application data. class and style are forbidden, including inside html:.NitroKit::Component::RESERVED_DATA_ATTRIBUTES: data-nk, data-slot, data-variant, data-size, data-nk-escape, data-enhanced, data-state, data-disabled, data-required, data-orientation, data-presentation, data-placement, data-layout, data-side, data-field-type, data-dir, data-gap, data-align, data-justify, data-wrap, data-cols, data-mode, data-key. Do not pass them through data:. data-action, data-controller are additive and compose with Nitro behavior.desperately_need_a_class:. It requires a non-blank String and marks the exception with data-nk-escape="class".data-nk; owned parts emit component-qualified data-slot values such as field-control or card-title. Select on those attributes, never on classes.--nk-* custom properties in an application stylesheet. Variables beginning with --_nk-* are private component mechanics.NitroKit::Flex and NitroKit::Grid for layout. Parents own external placement and available width; components own their intrinsic geometry.data-state when styling or behavior also needs it.