# Toast

> A queue of short messages in the corner of the screen. Standalone component.

The element is the container, not the message. You add it once in the app and call `show()` when there is something to report.

The container is a `role="status"` region, not `role="alert"`. A message that appears in the corner should not interrupt what the screen reader is doing. If the message is important enough to interrupt, it belongs in an [Alert](/components/alert.md) in the page itself.

```ts
// main.ts, runs once when the app starts
import { defineLtToast } from "@lo-tek/interfaces/toast"
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/toast.css"


defineLtToast()
```

## How to use it

The container is added once, and the messages come from your code. That is the difference from the other components: here you call a method instead of setting an attribute.

**Plain HTML**

```html
<!-- The stylesheets the page needs. Without a build step it is `<link>`, not
     `import`: a browser does not resolve a package name. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/toast/toast.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/button/button.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<lt-toast id="messages" class="lt-toast" role="status" aria-live="polite"
          aria-atomic="false"
          aria-label="Messages" data-ignore-morph></lt-toast>


<button class="lt-button" id="save" type="button">Save application</button>


<!-- Once for the whole app, in the page template. Not once per component. -->
<script type="module">
  import { defineLtToast } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/toast/lt-toast.js"


  defineLtToast()
</script>


<script type="module">
  const messages = document.getElementById("messages")


  document.getElementById("save").addEventListener("click", async () => {
    try {
      await save()
      messages.show("Your application has been saved", { color: "success" })
    } catch {
      messages.show("Could not save. Try again.", { color: "danger", duration: 0 })
    }
  })
</script>
```

An error message stays until the user closes it, hence `duration: 0`.

**React**

```ts
// src/lotek.d.ts, gives <lt-toast> types in JSX
import "@lo-tek/interfaces/react-jsx"
```

```tsx
// main.tsx, runs once when the app starts
import { defineLtToast } from "@lo-tek/interfaces/toast"
import "@lo-tek/interfaces/toast.css"
import "@lo-tek/interfaces/button.css"


defineLtToast()
```

Put the container in the root component, and share the reference through a context:

```tsx
import { createContext, useContext, useRef } from "react"
import { lt } from "@lo-tek/interfaces/react"
import { type LtToast, showToast } from "@lo-tek/interfaces/toast"
import "@lo-tek/interfaces/tokens.css"


const toasts = lt.toast({ label: "Messages" })


const Messages = createContext<(text: string, color?: "success" | "danger") => void>(
  () => {},
)


export function MessagesProvider({ children }: { children: React.ReactNode }) {
  const queue = useRef<LtToast>(null)


  const notify = (text: string, color?: "success" | "danger") => {
    if (!queue.current) return
    showToast(queue.current, text, { color, duration: color === "danger" ? 0 : undefined })
  }


  return (
    <Messages.Provider value={notify}>
      {children}
      <lt-toast ref={queue} {...toasts.host} />
    </Messages.Provider>
  )
}


export const useMessages = () => useContext(Messages)
```

```tsx
const notify = useMessages()


<button {...lt.button()} onClick={() => notify("Your application has been saved", "success")}>
  Save application
</button>
```

The messages live in the DOM, not in React state. That is deliberate: they should not disappear because a component re-rendered. `showToast()` waits until `defineLtToast()` has run, so a call made before registration is carried out as soon as the element is registered, instead of failing.

**Astro**

```astro
---
import { lt } from "@lo-tek/interfaces"
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/toast.css"


// Put the container in the page layout, so it exists on every page


const toasts = lt.toast({ label: "Messages" })


---


<slot />


<lt-toast id="messages" {...toasts.host}></lt-toast>


<script>
  import { defineLtToast } from "@lo-tek/interfaces/toast"
  defineLtToast()


  // Make the queue available to the islands on the page
  window.notify = (text, options) =>
    document.getElementById("messages").show(text, options)
</script>
```

The container is empty from the server, and is filled only by code running in the browser. It therefore costs nothing on a page that does not use it.

**Datastar**

```html
<!-- Datastar itself. Without it the `data-` attributes below do nothing. -->
<script type="module"
        src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>


<!-- The stylesheets the page needs. Without a build step it is `<link>`, not
     `import`: a browser does not resolve a package name. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/toast/toast.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/button/button.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->


<lt-toast id="messages" class="lt-toast" role="status" aria-live="polite"
          aria-atomic="false"
          aria-label="Messages" data-ignore-morph></lt-toast>


<button
  class="lt-button"
  type="button"
  data-on:click="document.getElementById('messages').show('Your application has been saved', { color: 'success' })"
>
  Save application
</button>


<!-- Once for the whole app, in the page template. Not once per component. -->
<script type="module">
  import { defineLtToast } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/toast/lt-toast.js"


  defineLtToast()
</script>
```

The messages are not state to be mirrored in a signal. They are something that happens, which is why `data-on:click` calls the method directly.

## The markup

A toast has three parts, and `lt.toast()` gives one attribute set for each. It is the same `toasts` as in the React and Astro recipes above:

```ts
const toasts = lt.toast({ label: "Messages" })
```

| Set | Which part | Who writes it |
| - | - | - |
| `toasts.host` | The container, that is `<lt-toast>` | **You**, once in the app |
| `toasts.message` | The message itself | The component, when you call `show()` |
| `toasts.close` | The close button in the message | The component |

That is why you spread `host` and not the whole object. The container is the only element you write:

```tsx
<lt-toast {...toasts.host} />
```

If you spread the whole of `toasts`, you set three attributes called `host`, `message` and `close`, none of which the browser knows. The container then has no `role="status"` until `defineLtToast()` has run, because the component fills in the role when it is missing. Until then no screen reader announces what appears in it, and `data-ignore-morph` never arrives, since that is the one thing the component does not set itself.

The other two sets are **names, not markup**. The component knows them and uses them on what it creates itself. You need them only when someone else writes the message: a Datastar app where the server patches a message straight into the region has to write `lt-toast__message` and `lt-toast__close` itself, and then they should have the same names as the ones the component uses.

Without a build step, you write the same set out as plain attributes, as the “Plain HTML” and “Datastar” recipes do:

```html
<lt-toast id="messages" class="lt-toast" role="status" aria-live="polite"
          aria-atomic="false"
          aria-label="Messages" data-ignore-morph></lt-toast>
```

`data-ignore-morph` is the most important of them, and the only one the component does not set itself.

`data-ignore-morph` is not optional. The component creates and removes the messages inside the region itself, and without the attribute an update from the server tears them away, because the server’s version of the region is empty. See [Markup and behaviour](/markup-and-behaviour.md#the-standalone-components).

## Attributes and methods

| Name | What it is |
| - | - |
| `duration` | Default lifetime in milliseconds. Without the attribute it is 6000. `0` leaves the messages in place. |
| `label` | Text that says what the region is. Becomes `aria-label`. Default: “Notifications” |
| `close-label` | What the close button in each message is called for the screen reader. Default: “Close message”. `lt.toast({ closeLabel })` writes it |
| `show(text, options)` | Shows a message and returns the element |
| `dismiss(message)` | Removes one message, and dispatches `toast-dismiss`. If the message had focus, focus moves to the message next to it |
| `clear()` | Removes all of them, without dispatching anything |

`showToast(element, text, options)` from `@lo-tek/interfaces/toast` does the same as `show()`, but first waits until the element is registered. Use it where the call can come before `defineLtToast()`, as in a module loaded alongside the registration.

The options for `show()`:

| Option | Default | Effect |
| - | - | - |
| `color` | `neutral` | `success`, `warning` or `danger` gives a coloured border |
| `duration` | from the element | Milliseconds before the message disappears |
| `closeLabel` | from `close-label` | What the close button is called for the screen reader, for this one message |

Both texts can be translated. See [Translation](/translation.md).

## Messages that disappear

A message that disappears by itself is an accessibility trap: someone who reads slowly, uses magnification or a screen reader may not have time to read it. The component does two things about that:

- The close button is always there.
- The timer stops while the mouse is over the message or focus is inside it, and only runs again when both are outside.

If the message needs to be readable later, it does not belong here. A receipt belongs in the page.

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-toast` | The region the messages are in, that is the host. It carries the placement in the corner |
| `.lt-toast__message` | One message. The colour comes from `data-color`. |
| `.lt-toast__close` | The close button |

| Variable | Default |
| - | - |
| `--lt-toast-space` | `var(--lt-spacing-4)` |
| `--lt-toast-width` | `min(24rem, calc(100vw - var(--lt-spacing-8)))` |
| `--lt-toast-padding` | `var(--lt-spacing-3) var(--lt-spacing-4)` |
| `--lt-toast-radius` | `var(--lt-spacing-1)` |
| `--lt-toast-layer` | `60` |

The container lets clicks through when it is empty, so it does not block buttons underneath it.

See [Customising](/customising.md) for how the variables and the layer work.

## Accessibility

- One thing at a time. Five messages on top of each other are read out one after another, and none of them gets attention.
- Do not put important actions in a message that disappears. The undo button belongs in the page.
- Errors that stop the user belong in an [Alert](/components/alert.md) or an [Error Summary](/components/error-summary.md), not in the corner.
- The text should say what happened, not just “Done”.
- **Not while a modal dialog is open.** Everything outside a modal dialog is inert: the message sits under the backdrop, the close button cannot be pressed, and the screen reader probably does not announce it. If the message concerns something the user is doing in the dialog, show it in the dialog, for example with an [Alert](/components/alert.md), or wait until the dialog is closed.
