Timed Message

A message that changes the longer something takes, so people know the app has not frozen.

import * as React from "react"

import { Button } from "@hel/ui/button"

When to use it

Use a timed message for work that can take a while and has no progress bar: an export, an upload, a payment. The first message says what is happening, and later ones reassure people as the wait goes on, or tell them what to do if it is taking too long. The times in these demos are short so you can watch them. In an app, you would use real ones, like 5 and 15 seconds.

For a single message that is still working, Shimmer Text is simpler. If you can show real progress, such as 40% uploaded, use a Progress bar instead.

Import

import { TimedMessage, useTimedStep } from "@hel/ui/timed-message"

Steps

steps is a list of messages. Each has text and an after, the milliseconds since the start when it takes over. The first step always shows from the start. The clock begins when the component appears, or when active turns on, and it goes back to the first step when active turns off.

<TimedMessage
  steps={[
    { text: "Preparing your export..." },
    { after: 5000, text: "Gathering the data. This can take a moment..." },
    { after: 15000, text: "Large exports take longer. Thanks for waiting." },
  ]}
/>

The messages are your own, so write them in your app's language. TimedMessage is a Text and takes the same size, tone, weight and align. Each new message slides up as the old one slides out, and people who prefer less motion see the text swap with no animation. Set animation={false} to turn the slide off for everyone, or give it a duration and easing to change it.

Examples

Something to do later

The message takes a function as its child, which is called with the step on show. Use it to put something next to the message from a certain step on, like an escape hatch once a wait gets long. The function gets index, step and last.

Uploading your file...

import * as React from "react"

import { Button } from "@hel/ui/button"

More than one line

When a stage is more than a single line of text, such as an icon, a title and a description that change together, use the useTimedStep hook. It takes the list of times and returns which step is on show, and you draw it however you like.

Processing your payment
This usually takes a few seconds.
import * as React from "react"
import { CreditCard, Landmark, ShieldCheck } from "lucide-react"

Screen readers

Each new message is announced politely, through role="status". Set announce={false} when something around it already announces the change, so it is not read twice.

Props

PropTypeDefault
steps{ after?: number, text: ReactNode }[]required
activebooleantrue
onStepChange(index: number) => voidnone
announcebooleantrue
animation{ duration?: number, easing?: string } or false400ms, eased
children({ index, step, last }) => ReactNodenone

onStepChange is called when another step takes over, but not for the first one at the start. It also takes every other prop of Text, apart from children.

useTimedStep

const step = useTimedStep([0, 5000, 15000], active)

It takes the times in milliseconds and an optional active, and returns the index of the step on show. It works out the step from the start time, so a tab that was in the background shows the right step when you come back.