Components
Toast
A toast is raised, not placed: Toast.Success("Saved") from any handler, with nothing injected. Ui.Toast in the layout is where toasts appear, and the last example below places the one every toast on this page shows in.
Drawn and behaving as Flux UI's toast does, measured against its live docs.
Toast
Toast.Info, Toast.Success, Toast.Warning and Toast.Error say it; Flux's Flux::toast().
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastExample : Component
{
protected override Component? Render() =>
Ui.Button.Primary.Id("toast-plain").OnClick(() => Toast.Info("Your changes have been saved."))["Save changes"];
}With heading
A heading gives the toast more context; the message becomes the line under it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastHeadingExample : Component
{
protected override Component? Render() =>
Ui.Button.Id("toast-heading")
.OnClick(() => Toast.Info("You can always update this in your settings.").Heading("Changes saved"))["With heading"];
}Variants
Success, warning and danger, each said by the shape of its icon as well as its colour.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastVariantsExample : Component
{
protected override Component? Render() =>
Div.Class("flex flex-wrap gap-2")[
Ui.Button.Key("success").Id("toast-success")
.OnClick(() => Toast.Success("The post has been created successfully.").Heading("Post created"))["Success"],
Ui.Button.Key("warning").Id("toast-warning")
.OnClick(() => Toast.Warning("Your post has unsaved changes.").Heading("Unsaved changes"))["Warning"],
Ui.Button.Key("danger").Id("toast-danger")
.OnClick(() => Toast.Error("Your changes have not been saved.").Heading("Something went wrong"))["Danger"]
];
}Actions
One action: its handler runs, with a spinner while it does, and then the toast goes.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastActionsExample : Component
{
protected override Component? Render() =>
Ui.Button.Id("toast-action")
.OnClick(() => Toast.Success("Your updates are now live.").Heading("Changes saved")
.Action("Undo", () => Toast.Info("Changes undone.")))["With an action"];
}Links
A link that gives a clear next step, and stays a real link.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastLinksExample : Component
{
protected override Component? Render() =>
Ui.Button.Id("toast-link")
.OnClick(() => Toast.Success("Invoice created.")
.Link("View invoice", PageMeta.LinkTo(Routes.UiKitDataDisplayPage())))["With a link"];
}Duration
How long it stays before it goes by itself. Five seconds when it says nothing.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastDurationExample : Component
{
protected override Component? Render() =>
Ui.Button.Id("toast-brief")
.OnClick(() => Toast.Info("Your changes have been saved.").For(1.Second))["One second"];
}Permanent
UntilDismissed() keeps it until its close button, or Escape, takes it down.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastPermanentExample : Component
{
protected override Component? Render() =>
Ui.Button.Id("toast-permanent")
.OnClick(() => Toast.Info("Your changes have been saved.").UntilDismissed())["Permanent"];
}Placement
Flux's inverted look, another corner, and a stack: what the layout places decides. Pick one, then raise toasts from the examples above.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class ToastPlacementExample : Component
{
private static readonly (Layout Layout, string Label)[] Layouts =
[
(Layout.Default, "Ui.Toast"),
(Layout.Inverted, "Ui.Toast.Invert()"),
(Layout.TopEnd, "Ui.Toast.TopEnd"),
(Layout.Stack, "Ui.ToastGroup[Ui.Toast]"),
(Layout.Expanded, "Ui.ToastGroup.Expanded()[Ui.Toast]"),
];
private Layout _layout;
private enum Layout
{
Default,
Inverted,
TopEnd,
Stack,
Expanded,
}
protected override Component? Render() =>
Div.Class("flex flex-wrap items-center gap-2")[
Layouts.Select(Choice),
// What an app writes once, in its layout. Every toast raised on this page shows in it.
_layout switch
{
Layout.Inverted => Ui.Toast.Invert(),
Layout.TopEnd => Ui.Toast.TopEnd,
Layout.Stack => Ui.ToastGroup[Ui.Toast],
Layout.Expanded => Ui.ToastGroup.Expanded()[Ui.Toast],
_ => Ui.Toast,
}
];
private Component Choice((Layout Layout, string Label) choice)
{
var button = Ui.Button.Key(choice.Label).Sm.Id("toast-layout-" + choice.Layout.ToString().ToLowerInvariant())
.OnClick(() => _layout = choice.Layout);
return (choice.Layout == _layout ? button.Primary : button.Outline)[choice.Label];
}
}Behaviour
Where toasts appear
Toast.Success("Saved") from any handler raises one. A toast belongs to the session, not the page, so one raised just before navigating shows once on the page the reader lands on.
Ui.Toast in the layout is where they appear. An app that places none gets the host's own, this one in the bottom end corner, moved with c.Toasts.At(Ui.ToastPosition.TopEnd).For(8.Seconds) or Rask:Toasts in appsettings; placing one replaces it.
A toast sits 24px from the two viewport edges it is against, which is where Flux puts one, and start and end follow the reading direction, so a right-to-left page mirrors. Ui.Toast.Class("pt-24") adds to each toast's own classes, to clear a nav bar at the top.
Flux's calls in C#
Flux | What happens |
|---|---|
<flux:toast /> in the layout | Ui.Toast in the layout, or nothing: the host places one. |
<flux:toast position="top end" invert /> | Ui.Toast.TopEnd.Invert(). Ui.ToastPosition is BottomEnd, BottomCenter, BottomStart, TopEnd, TopCenter or TopStart. |
<flux:toast class="pt-24" /> | Ui.Toast.Class("pt-24") |
<flux:toast.group> | Ui.ToastGroup[Ui.Toast] |
<flux:toast.group expanded position="top end"> | Ui.ToastGroup.Expanded().TopEnd[Ui.Toast] |
Flux::toast('Saved.'), $flux.toast('Saved.') | Toast.Info("Saved.") |
heading: 'Changes saved' | Toast.Info("…").Heading("Changes saved") |
variant: 'success', 'warning', 'danger' | Toast.Success(…), Toast.Warning(…), Toast.Error(…) |
duration: 1000, duration: 0 | .For(1.Second), .UntilDismissed() |
action: ['label' => 'Undo', 'event' => …] | .Action("Undo", UndoChanges) |
action: ['label' => 'View', 'href' => …] | .Action("View", Routes.InvoicePage(id)) |
link: ['label' => 'View invoice', 'href' => …] | .Link("View invoice", Routes.InvoicePage(id)) |
One at a time, or a stack
On its own, Ui.Toast shows one toast at a time: a new one takes the place of the one showing, and dismissing it takes those it replaced with it.
Inside a Ui.ToastGroup they stack: three show at rest, the newest in front and each older one a step back and a little narrower, and the pointer over the stack lays them all out, as Expanded() does for good.
How long it stays
A toast goes after five seconds unless it says otherwise, by its close button, or (on its own, not in a stack, as in Flux) by Escape. The time is counted in the browser, so it waits while the pointer is over the toast, and only the pointer: as on Flux, a toast with focus on its close button still goes on time. Once the time is up it fades for 350ms before it goes.
The kit ships no script, so all of that is the runtime's generic hooks, written as attributes. The host is a native popover the runtime shows (data-rask-popover-open), so a toast is in the top layer, over an open dialog. data-rask-dismiss-after="<ms>" has the runtime press the toast's own [data-rask-dismiss] button when the time is up, data-rask-dismiss-hold="pointer" says only the pointer holds it, and data-rask-shortcut="escape" is Escape.
A Ui.ToastGroup is one data-rask-dismiss-scope: the pointer anywhere over the stack holds every toast in it, and when it leaves each runs on from where it stopped rather than starting again.
Pressing the button rather than hiding the element is the point: the outlet takes the toast off its list, so the next render agrees with the screen. That timed press is sent as the runtime's own, so it carries none of the reader's unsent typing ahead of it.
The host is always on the page, closed and empty while there is nothing to show, as Flux's is. A toast arriving, leaving or taking another's place is a change inside it, which the live diff ships as one insert or removal.
Actions and links
An action given a route stays a real link: a new tab, a copied address. An action given a handler is a button that shows a spinner while the handler runs (the runtime's data-loading), then takes the toast down.
What it announces
Like Flux's, the host is role="status" and each toast aria-atomic="true", so a toast is announced politely, rather than interrupting, and whole. A spinner tells a screen reader nothing and a colour is invisible to a reader who cannot tell it apart, so the variant is carried by its icon's shape as well as its colour: a check, a triangle, a circle with a mark.
The toasts Rask raises itself
A handler that throws, and a form save that fails, raise an error toast with a sentence of Rask's own, followed by the reference the failure is logged under. Each is a RaskString, in English and in Hungarian out of the box, and replaced like any other through localization.
String | What happens |
|---|---|
HandlerFailed | That did not work. Please try again. — Nem sikerült. Kérjük, próbálja újra. |
FormSaveFailed | The save failed. Please try again. — A mentés nem sikerült. Kérjük, próbálja újra. |
ErrorReference | The problem has been recorded. If it keeps happening, quote this reference: {0} — A hibát rögzítettük. Ha újra előfordul, erre az azonosítóra hivatkozzon: {0} |
Reference
Ui.Toast
Where the app's toasts appear. Placed once, in the layout; a toast is raised with Toast.Success("Saved").
Step | Takes | What it does |
|---|---|---|
.Position(…) | Ui.ToastPositionBottomEnd BottomCenter BottomStart TopEnd TopCenter TopStart Each is a step of its own too, unless an HTML tag has its name. | The corner they appear in. The bottom end when unset; a group around it decides instead. |
.Invert() | bool | Dark in light mode and light in dark, for every toast. |
Ui.ToastGroup
Wrapped around a Ui.Toast, toasts stack instead of taking one another's place.
Step | Takes | What it does |
|---|---|---|
.Position(…) | Ui.ToastPositionBottomEnd BottomCenter BottomStart TopEnd TopCenter TopStart Each is a step of its own too, unless an HTML tag has its name. | The corner the stack sits in. The bottom end when unset. |
.Expanded() | bool | Lays every toast out at all times, not only under the pointer. |