The Presence component enables smooth animations when components enter and exit the DOM, supporting both CSS animations and transitions with full lifecycle control.
When to Use
Animate component entrance and exit
Create smooth expand/collapse animations
Handle modal and dialog transitions
Animate list item additions and removals
Build custom animated components with mount/unmount states
Basic Usage
CSS Animation
CSS Transition
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
import { useState } from "react" ;
function AnimatedBox () {
const [ isOpen , setIsOpen ] = useState ( false );
return (
< div >
< button onClick = { () => setIsOpen ( ! isOpen ) } > Toggle </ button >
< Presence present = { isOpen } >
< div className = "animated-box" >
Content with animation
</ div >
</ Presence >
</ div >
);
}
// CSS
// .animated-box {
// animation: fadeIn 300ms ease-out;
// }
//
// .animated-box[data-state="unmountSuspended"] {
// animation: fadeOut 300ms ease-out;
// }
Component API
Whether the component should be present (mounted) or not
children
React.ReactElement | ((context: RenderPropContext) => React.ReactElement)
Element to animate. Can be a render function receiving animation state
variant
'animation' | 'transition'
default: "animation"
Type of CSS animation to use. ‘animation’ for @keyframes, ‘transition’ for CSS transitions
Callback fired when the exit animation completes and element unmounts
Always render the element, even when not present. Useful for testing or specific animation needs
Additional CSS classes to apply to the wrapper
Render Prop Context
Current presence state (mounted or not)
isPresentOrIsTransitionComplete
True if present or transition variant is complete
Whether the transition should start (for transition variant)
Data Attributes
The component adds these data attributes for styling:
data-state: Current state ("mounted" | "unmountSuspended" | "unmounted")
data-present: Whether currently present ("true" | undefined)
data-present-or-transition-complete: Transition completion state
data-transition: Transition state ("active" | "inactive") - only for variant="transition"
Examples
Fade Animation
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
import { useState } from "react" ;
function FadeExample () {
const [ show , setShow ] = useState ( false );
return (
< div >
< button onClick = { () => setShow ( ! show ) } > Toggle </ button >
< Presence present = { show } onExitComplete = { () => console . log ( "Exit complete" ) } >
< div className = "fade-box" >
I fade in and out
</ div >
</ Presence >
</ div >
);
}
@keyframes fadeIn {
from { opacity : 0 ; }
to { opacity : 1 ; }
}
@keyframes fadeOut {
from { opacity : 1 ; }
to { opacity : 0 ; }
}
.fade-box {
animation : fadeIn 300 ms ease-out ;
}
.fade-box [ data-state = "unmountSuspended" ] {
animation : fadeOut 300 ms ease-out ;
}
Slide and Fade
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function SlideExample ({ isOpen , onClose }) {
return (
< Presence present = { isOpen } onExitComplete = { onClose } >
< div className = "slide-panel" >
< h2 > Side Panel </ h2 >
< p > Content here </ p >
</ div >
</ Presence >
);
}
@keyframes slideIn {
from {
transform : translateX ( 100 % );
opacity : 0 ;
}
to {
transform : translateX ( 0 );
opacity : 1 ;
}
}
@keyframes slideOut {
from {
transform : translateX ( 0 );
opacity : 1 ;
}
to {
transform : translateX ( 100 % );
opacity : 0 ;
}
}
.slide-panel {
animation : slideIn 400 ms cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
}
.slide-panel [ data-state = "unmountSuspended" ] {
animation : slideOut 300 ms ease-in ;
}
Using Transition Variant
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
import { useState } from "react" ;
function TransitionExample () {
const [ isExpanded , setIsExpanded ] = useState ( false );
return (
< div >
< button onClick = { () => setIsExpanded ( ! isExpanded ) } > Expand </ button >
< Presence present = { isExpanded } variant = "transition" >
< div className = "expandable" >
< p > This content expands smoothly </ p >
</ div >
</ Presence >
</ div >
);
}
.expandable {
max-height : 0 ;
opacity : 0 ;
overflow : hidden ;
transition :
max-height 300 ms ease-out ,
opacity 300 ms ease-out ;
}
.expandable [ data-transition = "active" ] {
max-height : 500 px ;
opacity : 1 ;
}
Render Prop Pattern
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function RenderPropExample ({ isVisible }) {
return (
< Presence present = { isVisible } >
{ ({ isPresent , shouldStartTransition }) => (
< div
className = "dynamic-box"
style = { {
opacity: isPresent ? 1 : 0 ,
transform: shouldStartTransition ? 'scale(1)' : 'scale(0.95)' ,
} }
>
Content
</ div >
) }
</ Presence >
);
}
Modal with Backdrop
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function Modal ({ isOpen , onClose , children }) {
return (
< Presence present = { isOpen } onExitComplete = { onClose } >
< div className = "modal-container" >
< div className = "modal-backdrop" onClick = { onClose } />
< div className = "modal-content" >
{ children }
</ div >
</ div >
</ Presence >
);
}
@keyframes fadeIn {
from { opacity : 0 ; }
to { opacity : 1 ; }
}
@keyframes slideUp {
from {
opacity : 0 ;
transform : translateY ( 20 px );
}
to {
opacity : 1 ;
transform : translateY ( 0 );
}
}
.modal-backdrop {
position : fixed ;
inset : 0 ;
background : rgba ( 0 , 0 , 0 , 0.5 );
animation : fadeIn 200 ms ease-out ;
}
.modal-content {
position : fixed ;
top : 50 % ;
left : 50 % ;
transform : translate ( -50 % , -50 % );
background : white ;
animation : slideUp 300 ms ease-out ;
}
Comparison to Native Patterns
Traditional Conditional Rendering
function Box ({ isOpen }) {
if ( ! isOpen ) return null ;
return < div className = "box" > Content </ div > ;
}
This provides no animation - the element immediately appears/disappears.
With Presence
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function Box ({ isOpen }) {
return (
< Presence present = { isOpen } >
< div className = "box" > Content </ div >
</ Presence >
);
}
The element smoothly animates in and out based on your CSS.
Common Use Cases
Collapsible Section
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
import { useState } from "react" ;
function Collapsible ({ title , children }) {
const [ isOpen , setIsOpen ] = useState ( false );
return (
< div className = "collapsible" >
< button onClick = { () => setIsOpen ( ! isOpen ) } >
{ title }
< span > { isOpen ? '−' : '+' } </ span >
</ button >
< Presence present = { isOpen } variant = "transition" >
< div className = "collapsible-content" >
{ children }
</ div >
</ Presence >
</ div >
);
}
Notification Toast
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function Toast ({ message , isVisible , onDismiss }) {
return (
< Presence present = { isVisible } onExitComplete = { onDismiss } >
< div className = "toast" >
{ message }
< button onClick = { onDismiss } > × </ button >
</ div >
</ Presence >
);
}
Animated List Items
import { Presence } from "@zayne-labs/ui-react/common/presence" ;
function AnimatedListItem ({ item , isVisible }) {
return (
< Presence present = { isVisible } >
< li className = "list-item" > { item . name } </ li >
</ Presence >
);
}
function List ({ items , visibleIds }) {
return (
< ul >
{ items . map (( item ) => (
< AnimatedListItem
key = { item . id }
item = { item }
isVisible = { visibleIds . includes ( item . id ) }
/>
)) }
</ ul >
);
}
Based on Radix UI Presence with enhanced variant support for both CSS animations and transitions.