jackbsteinberg / std-toast

120 stars 4 forks source link

A Standard 'Toast' UI Element

Introduction

This document proposes a new HTML element for a "toast" pop-up notification. It is provided as a built-in module.

This proposal is intended to incubate in the WICG once it gets interest on its Discourse thread. After incubation, if it gains multi-implementer interest, it will graduate to the HTML Standard as a new standard HTML element.

What is a "toast" pop-up notification?

"Toasts are pretty common in UX design; refers to popup notifications which typically appear at the bottom of the screen (like a piece of toast in a toaster)."

Kyle Decker

Sneha Munot provides a nice definition in her uxplanet.org article, where she compares toasts to dialog boxes.

[A toast] is a small message that shows up in a box at the bottom of the screen and disappears on its own after few seconds. It is a simple feedback about an operation in which current activity remains visible and interactive. It basically is to inform the user of something that is not critical and that does not require specific attention and does not prevent the user from using the app device.

For example; on gmail when a mail is send you receive a feedback of “Sending message…” written in the form of toast message.

Another concise definition is found in Ben Brocka's ux.stackexchange.com response:

A Toast is a non modal, unobtrusive window element used to display brief, auto-expiring windows of information to a user.

This adds which adds the distinguishing detail of a toast being auto-expiring.

In the absence of browser-intrinsic toasts, the current state of affairs is that many libraries and design systems include toast features. This repository contains a study group which surveys and compares toast implementations across the web and other platforms.

Below is an animated image displaying some typical toast behaviors, drawn from the Blueprint design component library. The study group contains a variety of such examples.

An animated demo showing several toasts in action, including close buttons, action buttons like "Retry", and multiple toasts stacking on top of, or replacing each other

Why?

Modern web applications allow users to complete many actions per page, which necessitates providing clear feedback for each action. Toast notifications are commonly used to unobtrusively provide this feedback.

Many libraries in a variety of frameworks implement a version of toast (see research), but the web has no built-in API to address the use case. By providing a toast API as part of the web platform's standard library, the web becomes more competitive with other app development platforms, and web application developers can spend less of their time and bytes on implementing this pattern.

Toasts are also a deceptively-tricky pattern to get right. They require special accessibility considerations, and scenarios involving multiple toasts need special handling. Not all libraries account for these subtleties. By providing a built-in toast control that fully handles these aspects, we can level up the typical toast experience for both developers and users of the web.

Finally, the ecosystem can benefit from a shared understanding of how to create and style toasts. If the platform provides a toast, then all libraries and components can freely use toasts to communicate to their users. Whereas, if toasts can only be found in libraries, then importing a toast-using component also means importing their opinion on what the best toast library is. In the worst case, this can lead to multiple uncoordinated toast libraries acting on a single page, each of which needs its own styling and tweaks to fit in to the application. If instead libraries and components all use the standard toast, the application developer can centrally style and coordinate them.

Sample code

The standard toast can be used according to two different patterns.

The first defines a <std-toast> HTML element, then shows it with configurations via a method on the element. This can be used to declaratively predefine toasts the application will need, and then show them inside the application logic.

<script type="module">
import 'std:elements/toast';
</script>

<std-toast id="sample-toast" type="success">
    Email sent!
</std-toast>
document.querySelector('#sample-toast').show({
    duration: 3000
});

The second imports the showToast() function from the "std:elements/toast" module, which takes in a message and some configurations and creates and shows a toast in the DOM. This is more convenient for one-off toasts, or for JavaScript-driven situations, similar to how the alert() function can be used show alerts.

import { showToast } from 'std:elements/toast';

const toast = showToast("Email sent!", {
    type: "success",
    duration: 3000
});

Goals

Across popular toast implementations there are recurring patterns, which the standard toast aims to accomplish natively.

The standard toast API hopes to provide a base for more opinionated or featureful toast libraries to layer on top of. It will be designed and built highly extensible, so library implementations can focus on providing more specific styling, better framework support, or more opinionated defaults. The intent is that any developer looking to use a toast in their work will use a standard toast, or a library which provides a wrapper on top of standard toast.

TODO(#14): create an example of this layering and link to it here.

Proposed API

The element is provided as a built-in module, named "std:elements/toast". See whatwg/html#4697 for more discussion on "pay-for-what-you-use" elements available via module imports.

The <std-toast> element

Behavior

The <std-toast> element provides a subtle, non-interruptive notification to the user. The toast will appear at a customized time and position on the screen, and will typically contain a message and optionally action and dismiss buttons (though arbitrary markup in the element is supported). The contents will be announced to a screen reader (politely or assertively depending on type [see attributes]), and after a certain duration, the toast will timeout and hide itself (though this timeout will be suspended while the toast has focus or the mouse is hovering on it).

TODO(#18, #29): determine properly accessible behavior, specifically w.r.t. actions and navigation to / from the toast.

Attributes

Properties

Reflected properties

All attributes will be reflected as properties on the element's JavaScript interface. For example:

const toast = document.createElement('std-toast');
console.log(toast.open); // false

The type enumerated attribute is reflected limited to only known values, with an invalid value default and missing value default of the default state. The default state does not have a corresponding keyword.

This means that toast.type will return the empty string, if the type="" attribute's value is missing, or not a case-insensitive match for "warning", "error", or "success". Otherwise it will return those values, in their canonical lowercase.

closeButton property

The closeButton property allows controlling the element's closebutton="" attribute. It is similar to the reflected properties, but slightly more complicated to allow both a boolean usage model (for using the user agent's default close button content) and a string usage model (for customizing the close button content). In detail:

To see examples of the closeButton setter in use, read on to the next section.

action property

There will additionally be an action property, which returns or allows setting an element that provides the toast's action. In detail:

To see examples of the action getter and setter in use, read on to the next section.

Contents

At a technical level, the <std-toast> element can be used as a generic container. However, the authoring conformance requirements, as well as the API, steer the developer in to the following content model:

TODO(#17): what about title or icon? They could potentially also be accomodated, in a similar fashion.

Thus, the following would all work well out of the box, and be comformant to the content model:

<std-toast>Hello world!</std-toast>

<std-toast><p>Hello <strong>world</strong>!</p></std-toast>

<std-toast id="toast3">
  Hello world!
  <button slot="action" class="fancy-button">Click me!</button>
</std-toast>
<script type="module">
document.querySelector("#toast3").action.onclick = e => { /*...*/ };
</script>

<std-toast>
  Hello world!
  <a slot="action" href="https://example.com/" target="_blank">Click me!</button>
</std-toast>

<std-toast closebutton>
  Hello world!
</std-toast>

<std-toast closebutton="Dismiss">
  Hello world!
</std-toast>

These can equivalently be created via JavaScript:

const toast1 = showToast("Hello world!");

const toast2 = showToast();
toast2.innerHTML = `<p>Hello <strong>world</strong>!</p>`;

const toast3 = showToast("Hello world!", { action: "Click me!" });
toast3.action.className = "fancy-button";
toast3.action.onclick = e => { /*...*/ };

const toast4 = showToast("Hello world!", { action: document.createElement("a") });
Object.assign(toast4.action,
  href: "https://example.com",
  target: "_blank",
  textContent: "Click me!"
});

const toast4SetterDemo = showToast("Hello world!");
toast4SetterDemo.action = document.createElement("a");
Object.assign(toast4SetterDemo.action, /* as before */);

const toast5 = showToast("Hello world!", { closeButton: true });

const toast6 = showToast("Hello world!", { closeButton: "Dismiss" });

(Note: because frames are only painted after JavaScript runs to completion, the setup code in the lines immediately after showToast() will be applied before the toast is ever shown to the user.)

More complex toasts, that don't fit the above content model, would require custom handling on the part of the developer, as they are stepping outside of the supported use case:

<!-- Invalid HTML, but will still "work" -->
<std-toast>
  <p>Hello world!</p>

  <textarea slot="action">Action 1</textarea>
  <button>Action 2</button>
  <button slot="action">Action 3</button>
</std-toast>

Such an unusual toast would still integrate with other toasts in terms of stacking and positioning behavior, and some of the default styles that are inherited may be useful. But the action property will only provide a pointer to the <textarea>, and the page author will need to add additional styling to handle the extra contents.

TODO: when we have a prototype, link to/show an example of this in action.

Methods

TODO(#37): should 3000 be the default? Should it be longer for accessibility reasons? Should it change dependent on the amount of text in the toast?

TODO(#19): how do multiple and newestOnTop work?

Events

A <std-toast> element can fire the following events:

showToast(message, options)

The "std:elements/toast" module also exports a convenience function, showToast(), which allows creating and showing toasts entirely from JavaScript. Behind the scenes, showToast() creates a <std-toast>, sets it up using the given message and options, inserts it as the last child of <body>, and then adds the open="" attribute, to make the toast visible. Finally, it returns the created <std-toast> element, allowing further manipulation by script.

message is a string that will be inserted as a text node into the created <std-toast>.

options allows configuring both the attributes and contents of the <std-toast>, as well as the options for this particular showing of the toast. Thus, the possible options are:

TODO(#15): Should a toast generated by showToast() get removed from the DOM, and if so should there be cases where it remains?

Default styles

The standard toast will come with these default styles, which developers will be able to change to customize look and feel. They are similar to the default styles for <dialog>, which are very minimal.

std-toast {
    position: fixed;
    offset-block-end: 1em;
    offset-inline-end: 1em;
    border: solid;
    padding: 1em;
    background: white;
    color: black;
    z-index: 1;
}

std-toast:not([open]) {
    display: none;
}

std-toast[type=success i] {
    border-color: green;
}

std-toast[type=warning i] {
    border-color: orange;
}

std-toast[type=error i] {
    border-color: red;
}

TODO: the choice of block/inline-end positioning (lower-right in left-to-right, horizontal writing modes) is not final and will likely be changed. It also interacts with the desire for easier positioning discussed in the attributes section. Discuss in #13 and #39.

Appearance customization

TODO: link to demos.

Appearance can be customized using normal CSS. For example, to change the colors, you could do

std-toast {
    border-color: blue;
    background-color: yellow;
    color: red;
}

To completely remove all default styling (useful for library authors), use

std-toast {
    all: unset;
}

Action

To style an action (if present), use the [slot=action] selector:

std-toast [slot=action] {
    color: blue;
    text-transform: uppercase;
}

(Note: we could offer an alternate syntax of std-toast::part(action) { ... }. Discuss in #20.)

Close button

To style the close button (if present via the closebutton="" attribute), use the ::part(closebutton) selector:

std-toast::part(closebutton) {
    position: absolute;
    top: 0;
    right: 0;
    background: none;
}

Types

To give custom styling to the different types of toasts, use the [type] selector. Remember to use the i modifier so that your styling works even if someone does <std-toast type="ERROR">.

std-toast[type=error i] {
    background: #c23934;
    color: white;
}

std-toast[type=error i]::before {
    content: "⛔" / "";
}

Accessibility

TODO: determine proper accessibility roles / semantics. See issues #25 and #29 for ongoing initial discussions.

Common Patterns

Show existing toast

<std-toast id="sample-toast">
    Hello World!
</std-toast>
document.querySelector('#sample-toast').show();

Create and show new toast with options

const toast = showToast("Hello World!", {
    type: "info",
    duration: 3000
});

Show toast in container

<div id="container"></div>
const toast = showToast("Hello World!");
document.querySelector("#container").append(toast);

Reusable config object

const configs = {
    type: 'info',
    duration: 3000
}

const toast1 = showToast("number 1", configs);
const toast2 = showToast("number 2", configs);

Security and Privacy Considerations

See TAG Security / Privacy Self Review.

Considered Alternatives

Extending the Notification API

Toasts are intended to be ephemeral, context-specific messages, and collecting them in a user's notifications tray doesn't reflect the spirit of the pattern. kenchris put this well in this comment on the TAG Design Review:

the difference is that this is a temporary notification / infobar, like you see for PWAs ("new version available, press reload to refresh") or like in GMail ("Chat is currently unavailable") etc. These are not things you want an actual system notification for... They really depend on the surrounding content/situation unlike regular notifications.

The toast is intended to be a completely in-page element, not a system-level notification the way Notifications and Android toasts are. Because of this page-level role, toasts do not require any user-granted permissions the way Notifications do.

Extending the <dialog> element

This approach was considered, and is still an open area of exploration, but our current thoughts are to go a different route for a few reasons. First and foremost, we felt the accessibility considerations for a toast and a dialog differ, as the dialog typically engages in the kind of flow-breaking behavior we want toasts to avoid. Additionally, the popular pattern of adding a call to action button on a toast with a baked-in time limit necessitates replacing tab-trapping with a less intrusive, easily restored alternative. The line between the elements becomes blurrier from a functional perspective, with action-button-containing toasts and non-modal dialogs, but the intent and semantics are sufficiently different, similar to <blockquote> and <q>, or <meter> and <progress>.

As a new global element (instead of a built-in module)

The idea of creating new polyfillable HTML elements is being explored in this issue on the HTML standard, and the idea of creating new pay-for-what-you-use HTML elements is being explored in this issue on the HTML standard.

Leaving this up to libraries

The rationale discussed for std-switch in this explanation in the Considered Alternatives section of tkent's std-switch explainer applies to std-toast as well.

Extra Resources

This proposal comes in parallel with the proposal for a standard switch element, which has a good FAQs section, many of which apply to this proposal as well. There is also currently an open discussion on the WICG discourse page to gauge and solicit community interest in moving the explainer to incubate in a WICG repo.