Message

A notice stands where its words apply: the failure beside the form that failed, the warning above the list it is about. It is not positioned and takes no box or shadow. A square mark, one control high and filled in the tone’s colour, carries the tone; the words sit on the paper beside it, each line one square, with the actions on the row below.

Message shows information on an app or page. Set form="inline", form="banner", form="toast", form="assistant" or form="app". The form describes where the message appears; tone describes what it means.

Feedback beside a proof

Success and warning messages share a face while their tone communicates the state.

Four tones

Danger is an alert, the others a status. The close button fires ds-message-dismiss and hides the notice.

<ds-message tone="danger" heading="Couldn't save" text="Check your connection and try again." dismissible>
<ds-button slot="actions" secondary>Try again</ds-button>
</ds-message>

Say what happened and what to do about it, in plain words. A notice needs a title, a text or both: a one-line message is a text alone.

ToneMarkWordsRole
neutral (the default)The grey fill, with infoThe text colourstatus
successApple green, with checkThe text colourstatus
warningOrange, with warningThe text colourstatus
dangerThe danger colour, with warningThe danger colour (--ds-danger-color)alert

The element is a live region: it gives itself role="status", or role="alert" for danger, so a screen reader reads it when it appears. A role the page writes on the element is kept. icon names another icon from the library for the mark, such as lock for something that cannot be changed.

dismissible adds a close button at the end of the first line, labelled with the shared word “Close”. Pressing it fires ds-message-dismiss, which bubbles and is cancelable: unless a listener cancels it, the notice hides itself. Cancel it to keep the notice, or to remove it from the page’s own state. dismiss() asks in the same way.

notice.addEventListener("ds-message-dismiss", event => {
event.preventDefault();
store.dismiss(id);
});

banner makes the notice a strip one square high across the top of its positioning context: put it in a positioned container, or at the page root. It is filled in the tone’s colour, with the mark, the title and the text on one line, the actions at the end of the line and the close button last. It uses absolute positioning and reserves no room in the flow; leave a square for it where words stand under it.

A notice across the top

With banner, the notice is a strip at the top of this isolated preview, its action at the end of the line.

An action and a dismiss

The close button fires ds-message-dismiss and hides the banner.

<ds-message form="banner" tone="success" text="Your changes are saved" literal>
<a slot="actions" class="ds-link" href="/icons/">Browse icons</a>
</ds-message>

An action is a real link or button in the actions slot.

form="app" is the row at the top of a site that leads to its iPhone app: the app’s picture, its name (heading) with one line about it (text) in the muted ink under it, Open and the close. It stands in the flow above the site’s own header, on the paper, with a hairline at its foot.

Leading to the app

form="app" at the top of the page: drawn in iOS browsers other than Safari, which shows Apple's own banner. preview draws it here on any device.

<ds-message form="app" heading="Field notes" text="Notes for the day, kept on your phone" literal
src="/app-icon.png" app-id="1234567890" open="https://example.com/notes"></ds-message>

It decides for itself where it shows, before it draws, so it never pops in after the page:

WhereWhat shows
Safari on an iPhone or iPad, with app-idNothing of its own: it writes Apple’s <meta name="apple-itunes-app" content="app-id=…, app-argument=…"> and Safari draws its Smart App Banner.
Chrome, Firefox, Edge and iOS’s other browsersThe row.
A computer, Android, a page opened from the home screen, an app’s own web viewNothing.
Anywhere, once closedNothing again in this browser (remembered under remember, one name per app by default).

Safari reads the meta tag as the page loads, so a page that knows its app writes the tag in its own head; the element writes one only where the page did not. Without app-id (an app not yet in the App Store, with only app-url), Safari draws the row as the other browsers do.

Open is a link to the app’s store address (app-url, or the App Store’s own from app-id), which offers Open when the app is installed. With open, the link first tries that address (a universal link or the app’s own scheme, the page the person is on) and goes to the store when the page is still in view a moment later. preview draws the row on any device, for documentation.

AttributeWhat it does
headingThe app’s name.
textOne line about it.
srcThe app’s picture; the icon slot takes an element instead.
app-idThe App Store id: Apple’s banner and the store address.
app-urlThe store address, when it is not the App Store’s own.
openThe address that opens the app at this page.
rememberThe name the close is remembered by.
previewDraw it on any device.

It is three squares high: half a square above and below two squares of content. The picture fills two squares with a picture’s corners and a hairline; the name and the line take a square each on the sixteen, cut with an ellipsis; Open (the primary button, one control high) and the close each stand on the sixteen of the square in the middle of the two, the close in a one-square cell at the end; one square at each side. Every measure follows the scale. It is a region named by the app’s name.

AttributeTypeDefaultWhat it does
toneneutral, success, warning, dangerneutralThe mark’s colour and icon, and the role.
headingstringEmptyA title on the first line, in the medium face.
textstringEmptyThe text. The default slot takes its place.
literalbooleanfalseShow heading and text as written, never looked up.
iconstringThe tone’sAnother icon for the mark.
dismissiblebooleanfalseShow the close button.
bannerbooleanfalseA strip across the top of its positioning context.
forminline, banner, toast, assistant, appinlineWhere the message appears; app is the row leading to the site’s app (see above).
SlotWhat it holds
DefaultThe text, when it needs more than words.
headingThe title.
iconAn icon of the page’s own inside the mark.
actionsButtons or links, on the row under the words.

The mark is one control high (16px) in a one-square cell, so the words start one square in; the close button takes a one-square cell at the end of the first line. Each line of the title and the text is one square, and the actions share one row with half a square between them. The notice fills the width it is given. Tones set --ds-message-mark, --ds-message-mark-ink and --ds-message-ink.

import { Message } from "@0001-labs/design-system-1/DS1/react/message";
<Message tone="warning" heading="Edited somewhere else" onDismiss={() => setShown(false)}
actions={<><Button secondary onClick={keepMine}>Keep mine</Button><Button secondary onClick={loadTheirs}>Load theirs</Button></>}>
Your changes are safe on this device.
</Message>

Message renders ds-message with its words and actions in the light DOM and its role in the markup, so the server’s HTML is already a live region. onDismiss shows the close button and leaves removing the notice to the product. Register ds-message in the browser before hydration.

The app banner takes the same props: <Message form="app" heading="Field notes" text="Notes for the day, kept on your phone" src="/app-icon.png" appId="1234567890" open={deepLink} />.

An assistant message stays in the page. Supply its identity through heading or the icon slot, its response as content, and its next steps through actions. The app owns conversation state and requests.

An assistant message

The same Message face, with an identity and actions supplied by the app.

Toast messages can be persistent or timed. Persistent messages stay until dismissed (duration: Infinity, the default); timed messages close automatically after their duration in milliseconds, such as duration: 4000. Both have a close button by default. The same choice applies to success and error messages after a promise finishes. The toast form uses the existing Sonner-based queue, gestures and announcements. Put one host in the app; message() sends messages to it.

<ds-message form="toast" position="bottom-right" close-button></ds-message>
import { message } from "@0001-labs/design-system-1";
message("Review required", { duration: Infinity });
message.success("Changes saved", { duration: 4000 });
message.error("Could not save");
message.promise(save(), { loading: "Saving", success: "Saved", error: "Could not save" });
message.dismiss();
Persistent and timed toast messages

Persistent stays until dismissed. Timed closes after four seconds; hover to pause its timer. Close either with its close button or a swipe.

message() supports the existing toast() options, including descriptions, actions, IDs, duration and promise updates. In React, import Message and message from DS1/react/message; register ds-message in the browser before hydration.

AttributeDefaultWhat it does (as in Sonner)
positionbottom-righttop-left, top-center, top-right, bottom-left, bottom-center or bottom-right.
expandOffKeeps the toasts spread out instead of stacked.
visible-toasts3How many toasts show at once.
durationInfinityUse Infinity to stay until dismissed, or milliseconds (for example 4000) to close automatically.
close-buttonOnAdds a close button to every toast.
rich-colorsOffFills success, info, warning and error toasts with their colour.
containedOffNot in Sonner: draws the toasts inside the nearest positioned parent, such as a panel’s status area, instead of the window.
labelMessagesThe region’s accessible name; pass the product’s own words.
close-labelCloseThe close button’s accessible name.

id, description, duration (Infinity keeps it), icon (a Design system 1 icon name), action and cancel ({ label, onClick }; the toast closes after the click unless onClick calls preventDefault(); with href, not in Sonner, the button is a link and closes the toast as it is followed), dismissible, closeButton, important (announced assertively; an error is unless it says false), onDismiss and onAutoClose. message.promise() also takes loading, success, error (text, or a function of the result) and finally.

As in Sonner: the newest toast is in front, the next two peek out behind it, and hovering or focusing the stack spreads them out and pauses their time, as does holding a toast or hiding the tab. A swipe towards the edge dismisses a toast; Alt+T moves focus to the toasts and Escape lets go. Loading toasts stay until they are updated or dismissed.

A toast is drawn as Design system 1’s label, not as Sonner’s card: an ink chip one control high for each line of words, in the tooltip’s colours (black with white words, turning over in dark mode), padded 4px, with no line and no shadow. It is as wide as its words, up to sixteen squares. Actions and Cancel are words of the same size, underlined. The pile advances one square per toast, a control and the rest of the square, and stands one square clear of the edge and the phone’s safe area. Only transform and opacity animate; a toast stacked behind takes the front one’s height at once. Swiping works towards the vertical edge only. Moving the element to another parent keeps its toasts and the time each had left. An error is announced assertively unless it says important: false.

Set --ds-toast-width, --ds-toast-background and --ds-toast-color on ds-message with shared tokens to fit a surface. Copy message through Sync 1.

ds-notice, Notice, ds-toast, Toaster and toast() remain supported for existing apps. They are compatibility entry points for Message. The native CSS classes remain ds-notice and ds-notice__*.