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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then 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.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/momentIts 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.10import { 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
Emptyinstead for a plain empty list with an icon and no art,Alertfor a message about something that happened beside the content,Skeletonfor the shape of content that is loading, andtoast()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,errororaskmoment 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 withcurrentColorand 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 adismissKeyoridits 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
timestimesperday, week, 30-day month or ever, counted inlocalStorageunder the moment'sdismissKey, or itsid; after that the moment is quiet. Without a key the flourish shows every time the moment appears. celebrate- Type:
boolean - Default:
trueforkind="success", otherwisefalse - 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; usekindandquiet. 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
dismissKeylasts: 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
localStorageasouro-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 withsize="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 (welcomeis the same kind by its older name, and the root saysfirst-run).success: something finished, which celebrates (seecelebrate).ask: something the app, or an agent working for the person, asks them to look at; show it withsize="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'scurrentColorfrom the theme and is on the root asdata-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'squiet, elseOuroProvider's, elsefalse - 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'squietsets it for the whole app, andMomentProviderfor 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" defaultstacks the art, title, line and actions in the space it is given.herodoes too, with art up to 220px tall, for the main state of a large, otherwise empty pane; narrower than 24rem it holds at 96px, asdefaultdoes.inlineis 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 usesactionandsecondaryAction:<Moment action={<Button>Retry</Button>}>.body: Not a prop. Ouro UI usesdescription:<Moment description="Messages you send show up here.">.heading: Not a prop. Ouro UI usestitle:<Moment title="No messages yet">.icon: Not a prop. Ouro UI usesart:<Moment art={<Illustration />}>.illustration: Not a prop. Ouro UI usesart:<Moment art={<Illustration />}>.onClose: Not a prop. Ouro UI usesonDismiss:<Moment onDismiss={() => setOpen(false)}>.variant: Not a prop. Ouro UI useskind:<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
- layout-classes: test:registry/ouro/components/ouro/moment/moment.test.tsx::Moment > passes attributes and a ref through, and lets layout classes win
verify:testExists(registry/ouro/components/ouro/moment/moment.test.tsx::Moment > passes attributes and a ref through, and lets layout classes win)
