Alert

Alert component is used to display important messages to the user.

export const Example = () => {
  return (
    <Alert side={<CircleAlert />} dismissible>
      <AlertTitle>Alert!</AlertTitle>
      <AlertDescription>
        This is an alert component. It's used to display important messages to
        the user.
      </AlertDescription>
    </Alert>
  );
};

Installation

If you haven't included Piksel UI in your project, include it first. You can check the installation page.

Source code

Copy the following source code and add it to your project.

"use client";

import { cn } from "@/utils/cn";
import { isRenderable } from "@/utils/is-renderable";
import { Slot, Slottable } from "@radix-ui/react-slot";
import { X } from "lucide-react";
import React, {
  createContext,
  forwardRef,
  HTMLAttributes,
  useContext,
  useState,
} from "react";
import { tv, VariantProps } from "tailwind-variants";

type AlertContextType = VariantProps<typeof alertStyles>;
const AlertContext = createContext<AlertContextType | null>(null);
const AlertContextProvider = AlertContext.Provider;
const useAlertContext = () => {
  const context = useContext(AlertContext) || {};
  return context;
};

export const alertDisplayName = "Alert";
export type AlertProps = HTMLAttributes<HTMLDivElement> &
  VariantProps<typeof alertStyles> & {
    /**
     * If true, the alert will be rendered as a child component.
     */
    asChild?: boolean;
    /**
     * Determines whether the alert is open or not.
     */
    isOpen?: boolean;
    /**
     * Callback to close the alert. If provided, the alert will be dismissable.
     */
    close?: () => void;
    /**
     * The content to be displayed on the left side of the alert.
     */
    side?: React.ReactNode;
  };
/**
 * @name Alert
 * @description Alert component is used to display important messages to the user.
 */
export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
  const {
    asChild,
    isOpen,
    close,
    side,
    variant,
    color,
    size,
    radius,
    dissmisable,
    className,
    children,
    ...rest
  } = props;
  const [open, setOpen] = useState(isOpen ?? true);
  const _open = isOpen ?? open;
  const _close = close ?? (() => setOpen(false));
  const _dissmisable = dissmisable ?? !!close;

  const { base, side: sideStyles } = alertStyles({
    variant,
    color,
    size,
    radius,
    dissmisable: _dissmisable,
  });

  const Component = asChild ? Slot : "div";

  if (!_open) return null;

  return (
    <AlertContextProvider
      value={{
        variant,
        color,
        size,
        radius,
        dissmisable,
      }}
    >
      <Component
        className={cn(base({ className }))}
        role="alert"
        {...rest}
        ref={ref}
      >
        {isRenderable(side) && <div className={sideStyles()}>{side}</div>}
        <div className="flex flex-col">
          <Slottable>{children}</Slottable>
        </div>
        {_dissmisable && (
          <button
            type="button"
            className="absolute size-8 top-0 end-0 inline-flex items-center justify-center text-white"
            onClick={_close}
          >
            <X size={20} />
          </button>
        )}
      </Component>
    </AlertContextProvider>
  );
});
Alert.displayName = alertDisplayName;

export const alertTitleDisplayName = "AlertTitle";
export type AlertTitleProps = HTMLAttributes<HTMLHeadingElement> & {
  /**
   * If true, the alert will be rendered as a child component.
   */
  asChild?: boolean;
};
/**
 * @name AlertTitle
 * @description The AlertTitle component is used to display the title of the alert.
 */
export const AlertTitle = forwardRef<HTMLHeadingElement, AlertTitleProps>(
  (props, ref) => {
    const { asChild, className, children, ...rest } = props;
    const context = useAlertContext();
    const { alertTitle } = alertStyles(context);

    const Component = asChild ? Slot : "h5";

    return (
      <Component
        className={cn(alertTitle({ className }))}
        role="heading"
        {...rest}
        ref={ref}
      >
        {children}
      </Component>
    );
  }
);
AlertTitle.displayName = alertTitleDisplayName;

export const alertDescriptionDisplayName = "AlertDescription";
export type AlertDescriptionProps = HTMLAttributes<HTMLParagraphElement> & {
  /**
   * If true, the alert will be rendered as a child component.
   */
  asChild?: boolean;
};
/**
 * @name AlertDescription
 * @description The AlertDescription component is used to display the description of the alert.
 */
export const AlertDescription = forwardRef<
  HTMLParagraphElement,
  AlertDescriptionProps
>((props, ref) => {
  const { asChild, className, children, ...rest } = props;
  const context = useAlertContext();
  const { alertDescription } = alertStyles(context);

  const Component = asChild ? Slot : "p";

  return (
    <Component
      className={cn(alertDescription({ className }))}
      {...rest}
      ref={ref}
    >
      {children}
    </Component>
  );
});
AlertDescription.displayName = alertDescriptionDisplayName;

export const alertStyles = tv({
  /**
   * Base styles of the alert
   */
  base: "relative flex items-start max-w-full overflow-hidden",
  slots: {
    alertTitle: "tracking-tight",
    alertDescription: "",
    side: "shrink-0 mt-px",
  },
  variants: {
    /**
     * Variant prop of the alert
     */
    variant: {
      filled: { base: "border" },
      soft: {},
      outline: { base: "border" },
    },
    /**
     * Color prop of the alert
     */
    color: {
      blue: {},
      red: {},
      indigo: {},
      green: {},
      yellow: {},
      zinc: {},
      white: {},
      black: {},
    },
    /**
     * Size prop of the alert
     */
    size: {
      sm: {
        base: "p-3 gap-2",
        alertTitle: "text-base font-semibold mb-1 last:mb-0",
        alertDescription: "text-sm mb-0.5 last:mb-0",
      },
      md: {
        base: "p-4 gap-3",
        alertTitle: "text-base font-semibold mb-1.5 last:mb-0",
        alertDescription: "text-base mb-0.5 last:mb-0",
      },
      lg: {
        base: "p-5 gap-4",
        alertTitle: "text-lg font-semibold mb-2 last:mb-0",
        alertDescription: "text-base mb-0.5 last:mb-0",
      },
    },
    radius: {
      none: { base: "rounded-none" },
      sm: { base: "rounded-sm" },
      md: { base: "rounded-md" },
      lg: { base: "rounded-lg" },
      xl: { base: "rounded-xl" },
    },
    dissmisable: {
      true: {},
    },
  },
  compoundVariants: [
    /* -------------------------------------------------------------------------- */
    /*                             Variants and colors                            */
    /* -------------------------------------------------------------------------- */
    /* -------------------------------------------------------------------------- */
    /*                               Filled variant                               */
    /* -------------------------------------------------------------------------- */
    {
      variant: "filled",
      color: "blue",
      className: { base: "bg-blue-500 text-white hover:text-white" },
    },
    {
      variant: "filled",
      color: "red",
      className: { base: "bg-red-500 text-white hover:text-white" },
    },
    {
      variant: "filled",
      color: "indigo",
      className: { base: "bg-indigo-500 text-white hover:text-white" },
    },
    {
      variant: "filled",
      color: "green",
      className: { base: "bg-green-500 text-white hover:text-white" },
    },
    {
      variant: "filled",
      color: "yellow",
      className: { base: "bg-yellow-400 text-black hover:text-black" },
    },
    {
      variant: "filled",
      color: "zinc",
      className: { base: "bg-zinc-500 text-white hover:text-white" },
    },
    {
      variant: "filled",
      color: "white",
      className: { base: "bg-zinc-50 text-zinc-950 hover:text-zinc-950" },
    },
    {
      variant: "filled",
      color: "black",
      className: {
        base: "bg-zinc-950 border-zinc-900 hover:text-white",
        alertTitle: "text-zinc-50",
        alertDescription: "text-zinc-200",
        side: "text-zinc-50",
      },
    },
    /* -------------------------------------------------------------------------- */
    /*                                Soft variant                                */
    /* -------------------------------------------------------------------------- */
    {
      variant: "soft",
      color: "blue",
      className: { base: "bg-blue-100 text-blue-600 hover:text-blue-600" },
    },
    {
      variant: "soft",
      color: "red",
      className: { base: "bg-red-100 text-red-600 hover:text-red-600" },
    },
    {
      variant: "soft",
      color: "indigo",
      className: {
        base: "bg-indigo-100 text-indigo-600 hover:text-indigo-600",
      },
    },
    {
      variant: "soft",
      color: "green",
      className: { base: "bg-green-100 text-green-600 hover:text-green-600" },
    },
    {
      variant: "soft",
      color: "yellow",
      className: {
        base: "bg-yellow-100 text-yellow-600 hover:text-yellow-600",
      },
    },
    {
      variant: "soft",
      color: "zinc",
      className: { base: "bg-zinc-100 text-zinc-600 hover:text-zinc-600" },
    },
    {
      variant: "soft",
      color: "white",
      className: { base: "bg-zinc-50 text-zinc-950" },
    },
    {
      variant: "soft",
      color: "black",
      className: {
        base: "bg-zinc-200 text-zinc-600",
        alertTitle: "text-zinc-950",
      },
    },
    /* -------------------------------------------------------------------------- */
    /*                               Outline variant                              */
    /* -------------------------------------------------------------------------- */
    {
      variant: "outline",
      color: "blue",
      className: {
        base: [
          "text-blue-500 outline-blue-500",
          "hover:bg-blue-500 hover:text-white",
          "focus-visible:ring-offset-blue-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "red",
      className: {
        base: [
          "text-red-500 outline-red-500",
          "hover:bg-red-500 hover:text-white",
          "focus-visible:ring-offset-red-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "indigo",
      className: {
        base: [
          "text-indigo-500 outline-indigo-500",
          "hover:bg-indigo-500 hover:text-white",
          "focus-visible:ring-offset-indigo-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "green",
      className: {
        base: [
          "text-green-500 outline-green-500",
          "hover:bg-green-500 hover:text-white",
          "focus-visible:ring-offset-green-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "yellow",
      className: {
        base: [
          "text-yellow-600",
          "hover:bg-yellow-500 hover:text-black",
          "focus-visible:ring-offset-yellow-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "zinc",
      className: {
        base: [
          "text-zinc-500",
          "hover:bg-zinc-500 hover:text-white",
          "focus-visible:ring-offset-zinc-300",
        ],
      },
    },
    {
      variant: "outline",
      color: "white",
      className: {
        base: [""],
      },
    },
    {
      variant: "outline",
      color: "black",
      className: {
        base: ["text-zinc-950"],
      },
    },
  ],
  defaultVariants: {
    variant: "filled",
    color: "black",
    size: "md",
    radius: "lg",
  },
});

Props

The Alert component has the following props.

PropTypeDefaultDescription
asChild?booleanundefinedIf true, the alert will be rendered as a child component.
variant?'filled' | 'soft' | 'outline''filled'The variant of the alert.
color?'blue' | 'red' | 'indigo' | 'green' | 'yellow' | 'zinc' | 'white' | 'black''black'The color of the alert.
size?'sm' | 'md' | 'lg''md'The size of the alert.
radius?'none' | 'sm' | 'md' | 'lg' | 'lg' | 'xl''lg'The border radius of the alert.
isOpen?booleanundefinedDetermines whether the alert is open or not.
close?() => voidundefinedCallback to close the alert. If provided, the alert will be dismissable.
side?React.ReactNodeundefinedThe content to be displayed on the left side of the alert.
dismissible?booleanundefinedIf true, the alert will be dismissable.