Skip to main content

Feedback

Moment

A moment in an app's life, shown in the space it is given: nothing here yet, on its way, something failed, a first run, or something finished. It holds art (any node, usually an inline SVG), a title, one line and up to two actions. It renders in the page's flow and never floats over what the person is using; its celebration stays inside the art's frame.

No messages yet

Messages you send show up here.

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/moment#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/moment

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/moment#v1.3.10 ourostack/ouro-ui/moment-tests#v1.3.10
import { Moment } from "@/components/ouro/moment/moment";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Moment
  art={<EmptyInbox />}
  title="No messages yet"
  description="Messages you send show up here."
  action={<Button onClick={compose}>New message</Button>}
/>
<Moment kind="success" art={<Done />} title="All caught up" dismissKey="caught-up" cap={{ times: 1, per: "day" }} />

Usage

Moments behave so they never get in the way: the art is decorative unless labelled, the art's own motion stops with reduced motion, the close button keeps a dismissal through dismissKey, cap limits how often a success celebrates, and quiet (or MomentProvider) makes them compact. The root carries data-moment (the kind), data-celebrate while it celebrates and data-quiet when quiet, and the art data-moment-art (its artName).

Use instead when

  • Use Empty instead for a plain empty list with an icon and no art, Alert for a message about something that happened beside the content, Skeleton for the shape of content that is loading, and toast() to confirm an everyday action.

Props

Type: MomentProps.

action
Type: ReactNode
The way forward, usually one Button: action={<Button onClick={compose}>New message</Button>}.
announce
Type: boolean
Speaks a success, error or ask moment politely to screen readers when it appears: the title, then the description when it is text. Other kinds are never spoken.
art
Type: ReactNode
The picture: any node, usually an inline SVG, which fills the art's height (160px, 96px when the moment is narrower than 24rem, 24px inline; none when quiet). It is decorative and hidden from screen readers unless you pass artLabel. Draw it with currentColor and the theme's variables so it follows the kind, the theme and dark mode. With reduced motion, its CSS animations stop and its SVG animations pause. Its motion has a budget too: its loops settle at the end of their turn about 5 s after it appears, and with a dismissKey or id its entrance plays once a session, so a moment that comes back as people switch views or filters arrives still.
artLabel
Type: string
Names the art for screen readers (role="img"), for a picture that says something the title does not.
artName
Type: string
Which picture this is, such as a pose ("waving", "pedalling"), for checkers, tests and styles: the art carries it as data-moment-art, which it has, empty, whenever there is art. Not shown or spoken.
cap
Type: { times: number; per: "day" | "week" | "month" | "ever" | (string & { 'Use "day", "week", "month" or "ever"': never }) }
Shows the flourish at most times times per day, week, 30-day month or ever, counted in localStorage under the moment's dismissKey, or its id; after that the moment is quiet. Without a key the flourish shows every time the moment appears.
celebrate
Type: boolean
Default: true for kind="success", otherwise false
A short flourish when the moment appears: a soft disc swells from behind the art and four sparks grow and fade, all inside the art's frame. With reduced motion only the disc fades. Never when quiet or inline. Capped with cap.
className
Type: string
Layout only: margins, width, grid placement (mt-6). Appearance classes warn in development; use kind and quiet.
description
Type: ReactNode
One line on what to do or what it means: "Messages you send show up here."
dismissFor
Type: "month" | "hour" | "day" | "week"
How long a dismissal through dismissKey lasts: an hour, a day, a week or 30 days; then the moment may show again. Without it a dismissal lasts for good. For an ask that may come back later, but not straight away.
dismissKey
Type: string
Keeps a dismissal: once the person closes the moment, it stays closed on every visit and in every tab, stored in localStorage as ouro-moment:dismissed:<dismissKey>. Shows the close button. With it the moment renders nothing until the browser has said whether it was dismissed, so a server-rendered page never flashes a closed moment.
dismissLabel
Type: string
Default: "Dismiss"
The close button's accessible name, for other languages.
headingLevel
Type: 2 | 3 | 4 | 5 | 6
Default: 3, and no heading with size="inline"
The title's heading level. Pick the level below the section the moment sits in. Inline, the title is plain text unless you pass one.
kind
Type: "loading" | "success" | "error" | "empty" | "first-run" | "welcome" | "ask"
Default: "empty"
What the moment is about. empty: nothing here yet. loading: on its way, with a spinner when there is no art. error: something failed. first-run: a first run or a welcome (welcome is the same kind by its older name, and the root says first-run). success: something finished, which celebrates (see celebrate). ask: something the app, or an agent working for the person, asks them to look at; show it with size="inline" in a slot of its own, such as a header strip or a notices area, never over the content. The kind colours the art's currentColor from the theme and is on the root as data-moment.
onDismiss
Type: (() => void)
Shows a quiet close button. Pressing it hides the moment, hands focus to what follows it, and calls this.
quiet
Type: boolean
Default: the nearest MomentProvider's quiet, else OuroProvider's, else false
Words only, in a compact row: the title and one line, with no art and no celebration (a loading moment keeps its small spinner). For dense panes, and for people who want less: OuroProvider's quiet sets it for the whole app, and MomentProvider for a part of it.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.
secondaryAction
Type: ReactNode
A second, quieter way, beside action: secondaryAction={<Button variant="plain">Learn more</Button>}. Two at most.
size
Type: "inline" | "default" | "hero"
Default: "default"
default stacks the art, title, line and actions in the space it is given. hero does too, with art up to 220px tall, for the main state of a large, otherwise empty pane; narrower than 24rem it holds at 96px, as default does. inline is a single row with 24px art, for a "working" row in a list or toolbar; in a slot too narrow for its words and its actions, the actions move under the words.
title (required)
Type: string
What happened or what is here, in a few words: "Inbox cleared". Required, because it is what a screen reader finds.

Native attributes

Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • actions: Not a prop. Ouro UI uses action and secondaryAction: <Moment action={<Button>Retry</Button>}>.
  • body: Not a prop. Ouro UI uses description: <Moment description="Messages you send show up here.">.
  • heading: Not a prop. Ouro UI uses title: <Moment title="No messages yet">.
  • icon: Not a prop. Ouro UI uses art: <Moment art={<Illustration />}>.
  • illustration: Not a prop. Ouro UI uses art: <Moment art={<Illustration />}>.
  • onClose: Not a prop. Ouro UI uses onDismiss: <Moment onDismiss={() => setOpen(false)}>.
  • variant: Not a prop. Ouro UI uses kind: <Moment kind="error">.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Moment merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Claim sources