Skip to main content
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

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

present
boolean
required
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
onExitComplete
() => void
Callback fired when the exit animation completes and element unmounts
forceMount
boolean
default:"false"
Always render the element, even when not present. Useful for testing or specific animation needs
className
string
Additional CSS classes to apply to the wrapper

Render Prop Context

isPresent
boolean
Current presence state (mounted or not)
isPresentOrIsTransitionComplete
boolean
True if present or transition variant is complete
shouldStartTransition
boolean
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 300ms ease-out;
}

.fade-box[data-state="unmountSuspended"] {
  animation: fadeOut 300ms 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 400ms cubic-bezier(0.16, 1, 0.3, 1);
}

.slide-panel[data-state="unmountSuspended"] {
  animation: slideOut 300ms 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 300ms ease-out,
    opacity 300ms ease-out;
}

.expandable[data-transition="active"] {
  max-height: 500px;
  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>
  );
}
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(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  animation: fadeIn 200ms ease-out;
}

.modal-content {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  animation: slideUp 300ms 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.

Build docs developers (and LLMs) love