Accessible React primitives for revealing rich content between a persistent header and footer.
Use reveal-ui for inline editors, expanding cards, comparison flows, and nested tasks where the surrounding context should remain visible.
npm install reveal-ui motion react react-domRequires React ^19.0.0 and Motion ^12.40.0.
import { RevealClose, RevealPanel, RevealTrigger } from 'reveal-ui'
export function ProfileCard() {
return (
<RevealPanel
content={
<div>
<label>
Display name
<input name="displayName" />
</label>
<RevealClose>Done</RevealClose>
</div>
}
>
<RevealPanel.Top>
<h2>Profile</h2>
<RevealTrigger>Edit</RevealTrigger>
</RevealPanel.Top>
<RevealPanel.Bottom>
<p>Your public account details.</p>
</RevealPanel.Bottom>
</RevealPanel>
)
}RevealPanel.Top and RevealPanel.Bottom stay mounted. The content section opens between them and unmounts after its closing transition.
onClose can return a promise. The panel waits for it before closing, ignores repeated close requests while it is pending, and stays open if it rejects. Rejections are normalized and displayed as panel errors.
import * as React from 'react'
import { RevealClose, RevealPanel, RevealTrigger } from 'reveal-ui'
export function ProfileEditor() {
const [name, setName] = React.useState('Ada')
return (
<RevealPanel
onClose={async () => {
const response = await fetch('/api/profile', {
method: 'POST',
body: JSON.stringify({ name }),
})
if (!response.ok) {
throw new Error('Could not save the profile.')
}
}}
onError={(error) => console.error(error)}
content={({ clearError }) => (
<div>
<label>
Display name
<input
value={name}
onChange={(event) => {
setName(event.target.value)
clearError()
}}
/>
</label>
<RevealClose>Save</RevealClose>
</div>
)}
>
<RevealPanel.Top>
<h2>{name}</h2>
<RevealTrigger>Edit</RevealTrigger>
</RevealPanel.Top>
<RevealPanel.Bottom>Profile settings</RevealPanel.Bottom>
</RevealPanel>
)
}For failures outside onClose, call reportError(value) from the content render props or useRevealPanelState(). Call clearError() to dismiss the current error.
An active error:
- keeps the panel open;
- renders a
role="alert"banner in the revealed content; - displays a decorative error badge in the header;
- adds
data-errorto the panel regions and controls; - clears when the panel closes successfully.
Strings, Error instances, and objects with a string message or error field are supported. Structured errors preserve title, code, and cause when provided.
type RevealError = {
message: string
title?: string
code?: string | number
cause?: unknown
}Pass error and onErrorChange to control the error from outside the panel.
const [open, setOpen] = React.useState(false)
const [error, setError] = React.useState<RevealError | null>(null)
<RevealPanel
open={open}
onOpenChange={setOpen}
error={error}
onErrorChange={setError}
content={<Editor />}
>
<RevealPanel.Top>
<RevealTrigger>Edit</RevealTrigger>
</RevealPanel.Top>
<RevealPanel.Bottom>Summary</RevealPanel.Bottom>
</RevealPanel>Errors clear only after the resolved open state changes to closed. If a controlled parent ignores onOpenChange(false), the panel remains open and keeps its error.
Wrap sibling panels in RevealGroup to close the others when one opens:
<RevealGroup>
<RevealPanel content={<FirstDetails />}>...</RevealPanel>
<RevealPanel content={<SecondDetails />}>...</RevealPanel>
</RevealGroup>If a sibling has an async onClose, it remains open until that callback resolves.
Inside a nested panel, use close({ propagate: true }) to close the current panel and its parent after a successful close.
The content render function and useRevealPanelState() share the same core state and actions:
function PanelStatus() {
const { phase, hasError } = useRevealPanelState()
return <p>{hasError ? 'Action failed' : `Panel is ${phase}`}</p>
}useRevealPanelState() must be called below a RevealPanel.
| Export | Purpose |
|---|---|
RevealPanel |
Main disclosure primitive |
RevealGroup |
Coordinates sibling panels |
RevealTrigger |
Opens its nearest panel |
RevealClose |
Closes its nearest panel |
useRevealPanelState() |
Reads panel state and actions |
CloseOptions |
Options accepted by close() |
RevealError |
Normalized panel error |
RevealPanelProps |
Props for RevealPanel |
RevealPanelState |
Value returned by the state hook |
RevealPhase |
'closed' | 'opening' | 'open' | 'closing' |
RevealRenderProps |
Value passed to a content render function |
RevealContentProp |
Accepted shape of the content prop |
RevealTriggerProps |
Props accepted by trigger and close controls |
RevealPanel also exposes Top, Bottom, Trigger, and Close as static composition helpers. RevealPanel.Trigger and RevealPanel.Close are aliases for the standalone controls.
| Prop | Description and type | Default |
|---|---|---|
children |
Persistent top and bottom regions. Type: ReactNode |
Required |
content |
Content revealed between the persistent regions. Type: ReactNode | (state) => ReactNode |
— |
revealContent |
Deprecated compatibility alias for content.Type: same as content |
— |
className |
Class name applied to the panel scope. Type: string |
— |
keepMounted |
Keeps closed content mounted and hidden so local state is retained. Type: boolean |
false |
autoSplit |
Infers top and bottom regions from unmarked children. Type: boolean |
false |
| Prop | Description and type | Default |
|---|---|---|
defaultOpen |
Sets the initial state of an uncontrolled panel. Type: boolean |
false |
open |
Controls the resolved open state. Type: boolean |
— |
onOpenChange |
Receives requests to change the open state. Type: (open: boolean) => void |
— |
onClose |
Runs before closing. A returned promise delays the close; rejection keeps the panel open and becomes a panel error. Type: (options?: CloseOptions) => void | Promise<void> |
— |
disabled |
Disables panel controls. Type: boolean |
false |
restoreFocusOnClose |
Returns focus to the last trigger after closing. Type: boolean |
true |
regionLabel |
Supplies a fallback accessible name when no trigger labels the content region. Type: string |
'Revealed content' |
closeSiblings |
Overrides whether opening this panel closes panels in the nearest group. Type: boolean |
Group setting, otherwise false |
containTriggers |
Prevents delegated controls from affecting nested panels. Type: boolean |
true |
triggerAttr |
Names the attribute used by delegated open controls. Type: string |
'data-trigger-collapse' |
restoreAttr |
Names the attribute used by delegated close controls. Type: string |
'data-trigger-restore' |
| Prop | Description and type | Default |
|---|---|---|
error |
Controls the normalized error shown by the panel. Type: RevealError | Error | string | null |
— |
onErrorChange |
Receives normalized error changes, including null when cleared.Type: (error: RevealError | null) => void |
— |
onError |
Runs whenever reportError() or a rejected onClose reports an error.Type: (error: RevealError) => void | Promise<void> |
— |
| Prop | Description and type | Default |
|---|---|---|
scrollOnOpen |
Scrolls the panel into view when it opens. Type: boolean |
false |
restoreScrollOnClose |
Restores the captured scroll position after closing. Type: boolean |
false |
scrollContainer |
Sets the primary scroll target directly or through a resolver. Type: HTMLElement | null | (() => HTMLElement | null) |
Nearest scroller |
scrollCascade |
Coordinates additional scroll containers. Type: Array<{ container; offset?; mode?; padding? }> |
[] |
scrollOffset |
Sets the offset from the scroll target's top edge. Type: number |
0 |
scrollDurationMs |
Sets the scroll animation duration in milliseconds. Type: number |
450 |
scrollSpacerTarget |
Chooses where temporary scroll space is added. Type: 'self' | 'container' | 'none' |
'self' |
scrollOvershootPx |
Sets the overshoot used during animated alignment. Type: number |
12 |
magicMotion |
Enables layout and parallax transitions. Type: boolean |
false |
parallaxOffset |
Sets the top and bottom translation distance in pixels. Type: number |
10 |
revealBlurPx |
Sets the content blur used during transitions. Type: number |
6 |
| Field | Type | Description |
|---|---|---|
isOpen |
boolean |
Resolved open state |
phase |
RevealPhase |
Current transition phase |
disabled |
boolean |
Whether panel controls are disabled; hook only |
contentId |
string |
Stable ID for the content region |
triggerId |
string | undefined |
ID of the active trigger |
open() |
() => void |
Opens the panel |
close(options?) |
(options?: CloseOptions) => void |
Requests a close |
error |
RevealError | null |
Current normalized error |
hasError |
boolean |
Whether an error is active |
reportError(value) |
(value: unknown) => void |
Reports and displays an error |
clearError() |
() => void |
Clears the error |
close() accepts { restoreFocus?: boolean, propagate?: boolean }.
RevealTrigger and RevealClose accept standard button props plus asChild. With asChild, props and behavior are merged into the child element through Radix Slot.
RevealGroup accepts children and an optional closeSiblings boolean, which defaults to true.
- Triggers receive
aria-expandedandaria-controls. - Revealed content uses
role="region"and is labelled by its active trigger when possible. - Error messages use
role="alert"; the header badge is hidden from assistive technology. - Focus returns to the last trigger by default.
- Reduced-motion preferences disable or simplify motion and scrolling.
- Delegated non-button controls receive button semantics and keyboard support.
Use the state attributes to style any panel state:
[data-reveal-scope][data-state='open'] { /* open panel */ }
[data-reveal-scope][data-phase='closing'] { /* closing panel */ }
[data-reveal-scope][data-error] { /* error panel */ }
[data-reveal-scope][data-disabled] { /* disabled panel */ }The scope, top region, revealed content, bottom region, and controls expose the relevant data-state, data-phase, data-error, and data-disabled attributes.
RevealSplitter was removed. Replace it with RevealPanel:
- import { RevealSplitter } from 'reveal-ui'
+ import { RevealPanel } from 'reveal-ui'npm install
npm run ci
npm run docs:previewMIT
