Back

Notification

A reusable notification component that displays animated messages with smooth slide and scale transitions, perfect for in-app alerts, toasts, and contextual updates.

Current Theme:

"Please direct your attention to the bottom-right corner of the viewport for a preview."

Nexus raises $40M Series B 🎉

Fueling the future of social identity and agent-driven play.

The new funding, led by Aurora Ventures and Vector Capital, will help us advance agent identity, expand cross-world integrations, and elevate the creator experience across the Nexus ecosystem.

API usage

components/notification.demo.tsx
import {
  Notification,
  NotificationCancel,
  NotificationContent,
  NotificationTrigger,
} from "./notification";

export function NotificationDemo() {
  return (
    <Notification
      className="right-5 bottom-5 z-50 max-w-xs text-sm"
      slideContentFrom="bottom"
    >
      <NotificationTrigger>
        <h3 className="font-medium">Nexus raises $40M Series B 🎉</h3>
        <p className="mt-1 text-xs">
          Fueling the future of social identity and agent-driven play.
        </p>
      </NotificationTrigger>
      <NotificationContent className="max-h-(--container-xs)">
        The new funding, led by Aurora Ventures and Vector Capital, will help us
        advance agent identity, expand cross-world integrations, and elevate the
        creator experience across the Nexus ecosystem.
      </NotificationContent>
      <NotificationCancel />
    </Notification>
  );
}

Installation

CLI (recommended)

1
Run the command below to add the component to your project.
It will also generate the required base stylesheet if one doesn't already exist and guide you through setting up the import alias @/components/... if it isn't already configured.
pnpm dlx shadcn@latest add https://100xui.com/components/notification.json

Manual

1
Install the following dependencies.
pnpm add clsx lucide-react motion tailwind-merge
2
Copy and paste the following code into your project.
100xui
components/notification.tsx
"use client";

import * as React from "react";
import {
  motion,
  useAnimate,
  type Transition,
  type HTMLMotionProps,
  type MotionStyle,
} from "motion/react";
import { XIcon } from "lucide-react";

import { cn } from "@/lib/utils";

interface NotificationContextType {
  dismissNotification: () => void;
  showNotification: () => Promise<void>;
  hideNotification: () => Promise<void>;
  scope: React.RefObject<HTMLDivElement>;
  expandedGap: React.CSSProperties["gap"];
  minVisibleContentHeight: React.CSSProperties["height"];
  initialContentScale: React.CSSProperties["scale"];
  slideFromTop: boolean;
}

const NotificationContext = React.createContext<NotificationContextType | null>(
  null,
);

function useNotification() {
  const ctx = React.useContext(NotificationContext);
  if (!ctx)
    throw new Error(
      "useNotification must be used within Notification Provider",
    );
  return ctx;
}

interface NotificationProps extends React.ComponentProps<"div"> {
  transition?: Partial<Record<"scale" | "translateY", Transition>>;
  initialContentScale?: React.CSSProperties["scale"];
  expandedGap?: React.CSSProperties["gap"];
  minVisibleContentHeight?: React.CSSProperties["height"];
  slideContentFrom?: "bottom" | "top";
}

export function Notification({
  transition: { scale: scaleTransition, translateY: translateYTransition } = {
    scale: { ease: "easeOut" },
    translateY: { ease: "easeOut" },
  },
  initialContentScale = 0.95,
  expandedGap = "calc(var(--spacing) * 2)",
  minVisibleContentHeight = "calc(var(--spacing) * 2)",
  onMouseLeave,
  className,
  children,
  slideContentFrom = "top",
  ...rest
}: NotificationProps) {
  const [isRendered, setIsRendered] = React.useState(true);
  const [scope, animate] = useAnimate();
  const slideFromTop = slideContentFrom === "top";

  const showNotification = async () => {
    await animate(scope.current, { y: "0px" }, translateYTransition);
    await animate(scope.current, { scale: 1 }, scaleTransition);
  };

  const hideNotification = async () => {
    await animate(
      scope.current,
      { scale: initialContentScale },
      scaleTransition,
    );
    await animate(
      scope.current,
      {
        y: `calc(${slideFromTop ? "1" : "-1"} * (-100% - ${expandedGap} + ${minVisibleContentHeight}))`,
      },
      translateYTransition,
    );
  };

  const dismissNotification = () => setIsRendered(false);

  return (
    <>
      {isRendered && (
        <NotificationContext.Provider
          value={{
            dismissNotification,
            showNotification,
            hideNotification,
            scope,
            expandedGap,
            minVisibleContentHeight,
            initialContentScale,
            slideFromTop,
          }}
        >
          <div
            className={cn("fixed isolate flex flex-col", className)}
            onMouseLeave={(e) => {
              hideNotification();
              onMouseLeave?.(e);
            }}
            {...rest}
          >
            {children}
          </div>
        </NotificationContext.Provider>
      )}
    </>
  );
}

export function NotificationTrigger({
  className,
  children,
  onMouseEnter,
  ...rest
}: React.ComponentProps<"div">) {
  const { showNotification, slideFromTop } = useNotification();

  return (
    <div
      onMouseEnter={(e) => {
        showNotification();
        onMouseEnter?.(e);
      }}
      className={cn(
        "bg-secondary text-secondary-foreground border-border/50 relative z-20 rounded-lg border p-3 shadow-md",
        className,
        slideFromTop ? "order-1" : "order-2",
      )}
      {...rest}
    >
      {children}
    </div>
  );
}

export function NotificationContent({
  children,
  className,
  style,
  ...rest
}: Omit<HTMLMotionProps<"div">, "ref">) {
  const {
    scope,
    expandedGap,
    minVisibleContentHeight,
    initialContentScale,
    slideFromTop,
  } = useNotification();

  const contentStyle: MotionStyle = {
    ...style,
    scale: initialContentScale,
    y: `calc(${slideFromTop ? "1" : "-1"} * (-100% - ${expandedGap} + ${minVisibleContentHeight}))`,
    transformOrigin: `50% ${slideFromTop ? "100%" : "0%"}`,
    [slideFromTop ? "marginTop" : "marginBottom"]: expandedGap,
  };

  return (
    <div
      className={cn(
        "pointer-events-none relative z-10 -m-2 flex overflow-hidden p-2",
        slideFromTop ? "order-2 pb-10" : "order-1 pt-10",
      )}
    >
      <motion.div
        style={contentStyle}
        ref={scope}
        className={cn(
          "bg-secondary text-secondary-foreground border-border/50 pointer-events-auto w-full overflow-y-auto rounded-lg border p-3 shadow-md",
          className,
        )}
        {...rest}
      >
        {children}
      </motion.div>
    </div>
  );
}

export function NotificationCancel({
  children = <XIcon className="size-3" />,
}: React.ComponentProps<"button">) {
  const { dismissNotification, hideNotification, slideFromTop } =
    useNotification();

  return (
    <button
      onClick={dismissNotification}
      onMouseEnter={hideNotification}
      className={cn(
        "bg-destructive text-primary-foreground absolute right-0 z-30 aspect-square translate-x-1/3 cursor-pointer rounded-full p-1",
        slideFromTop ? "top-0 -translate-y-1/3" : "bottom-0 translate-y-1/3",
      )}
    >
      {children}
    </button>
  );
}
3
Finally, Update the import paths to match your project setup.

API reference

<Notification/>

PropsTypeDescriptionDefault value
transition?
Partial<Record<"scale" | "translateY", Transition>>
Custom motion transitions for scale and vertical movement. Lets you fine-tune animation timing and easing.
{
  scale: { ease: "easeOut" },
  translateY: { ease: "easeOut" }
}
initialContentScale?
React.CSSProperties["scale"]
Sets the starting scale of the content before expansion. Adjust this to create a subtle zoom-in or pop effect when the notification opens.
0.95
expandedGap?
React.CSSProperties["gap"]
Specifies the gap between the trigger and the expanded content when the notification is open.
"calc(var(--spacing) * 2)"
minVisibleContentHeight?
React.CSSProperties["height"]
Determines how much of the content remains visible when the notification is collapsed. Useful for showing a preview.
"calc(var(--spacing) * 2)"
slideContentFrom?
"bottom" | "top"
Controls the direction from which the content slides in during expansion.
"top"
...rest
React.ComponentProps<"div">
Any standard React div props, like id, style or className, which will be applied directly to the component's root element.
undefined

<NotificationTrigger/>

PropsTypeDescriptionDefault value
props
React.ComponentProps<"div">
Any standard React div props like id, style, className, onMouseEnter, etc. These will be applied directly to the trigger element.
undefined

<NotificationContent/>

PropsTypeDescriptionDefault value
props
Omit<HTMLMotionProps<"div">, "ref">
Any standard motion.div props excluding ref. These will be applied to the content wrapper element, allowing custom styles, classNames, or event handlers.
undefined

<NotificationCancel/>

PropsTypeDescriptionDefault value
props
React.ComponentProps<"button">
Any standard React button props, like id, style, className, or onClick. Applied to the cancel button, with built-in dismiss and hide functionality.
undefined