Overlays
Dialog, AlertDialog, Drawer, Sheet, Popover, HoverCard, Tooltip.
Dialog
Modal dialog. Exports: Dialog, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, DialogClose.
Usage:
import {
Dialog, DialogTrigger, DialogContent, DialogHeader,
DialogTitle, DialogDescription, DialogFooter, DialogClose,
} from "@flow-industries/ui/components/dialog";
<Dialog>
<DialogTrigger render={<Button>Edit profile</Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update your public details.</DialogDescription>
</DialogHeader>
<Input placeholder="Name" />
<DialogFooter>
<DialogClose render={<Button variant="outline">Cancel</Button>} />
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>DialogContent includes a close button by default — pass showCloseButton={false} to hide it.
Source:
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "../../utils/cn";
import { Button } from "./button";
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-200 ease-(--ease-out-expo) supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm text-foreground ring-1 ring-primary/10 duration-200 ease-(--ease-out-expo) outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean;
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t-[length:var(--border-width)] bg-muted/50 p-4 sm:flex-row sm:justify-end",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
);
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-base leading-none font-medium tracking-tighter",
className,
)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};AlertDialog
Confirmation dialog variant. Outside clicks don't dismiss. Exports: AlertDialog, AlertDialogTrigger, AlertDialogPortal, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogMedia, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel.
Usage:
import {
AlertDialog, AlertDialogTrigger, AlertDialogContent,
AlertDialogHeader, AlertDialogTitle, AlertDialogDescription,
AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
} from "@flow-industries/ui/components/alert-dialog";
<AlertDialog>
<AlertDialogTrigger render={<Button variant="destructive">Delete</Button>} />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
<AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>Source:
"use client";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import type * as React from "react";
import { cn } from "../../utils/cn";
import { Button } from "./button";
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-200 ease-(--ease-out-expo) supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
size = "default",
...props
}: AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm";
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-foreground ring-1 ring-primary/10 duration-200 ease-(--ease-out-expo) outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn(
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
className,
)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t-[length:var(--border-width)] bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogMedia({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn(
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn(
"text-base font-medium tracking-tighter sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className,
)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
data-slot="alert-dialog-action"
className={cn(className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
variant = "outline",
size = "default",
...props
}: AlertDialogPrimitive.Close.Props &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<AlertDialogPrimitive.Close
data-slot="alert-dialog-cancel"
className={cn(className)}
render={<Button variant={variant} size={size} />}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};Drawer
Swipeable slide-in panel. Supports side = top / bottom / left / right. Base UI's Drawer — not vaul.
Usage:
import {
Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle,
} from "@flow-industries/ui/components/drawer";
<Drawer side="right">
<DrawerTrigger render={<Button>Open</Button>} />
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Settings</DrawerTitle>
</DrawerHeader>
<div className="p-4">...</div>
</DrawerContent>
</Drawer>Source:
"use client";
import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer";
import * as React from "react";
import { cn } from "../../utils/cn";
type DrawerSide = "top" | "bottom" | "left" | "right";
const DrawerContext = React.createContext<DrawerSide>("bottom");
const sideToSwipe = {
bottom: "down",
top: "up",
left: "left",
right: "right",
} as const;
function Drawer({
side = "bottom",
...props
}: Omit<DrawerPrimitive.Root.Props, "swipeDirection"> & {
side?: DrawerSide;
}) {
return (
<DrawerContext.Provider value={side}>
<DrawerPrimitive.Root
data-slot="drawer"
swipeDirection={sideToSwipe[side]}
{...props}
/>
</DrawerContext.Provider>
);
}
function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: DrawerPrimitive.Backdrop.Props) {
return (
<DrawerPrimitive.Backdrop
data-slot="drawer-overlay"
className={cn(
"fixed inset-0 z-50 bg-overlay transition-opacity duration-300 ease-(--ease-out-expo) supports-backdrop-filter:backdrop-blur-xs data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:transition-none",
className,
)}
style={{ opacity: `calc(1 * (1 - var(--drawer-swipe-progress, 0)))` }}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: DrawerPrimitive.Popup.Props) {
const side = React.useContext(DrawerContext);
const positionClasses = {
bottom:
"inset-x-0 bottom-0 max-h-[80vh] rounded-t-xl border-t-[length:var(--border-width)] [transform:translateY(var(--drawer-swipe-movement-y,0px))] data-ending-style:[transform:translateY(100%)] data-starting-style:[transform:translateY(100%)]",
top: "inset-x-0 top-0 max-h-[80vh] rounded-b-xl border-b-[length:var(--border-width)] [transform:translateY(var(--drawer-swipe-movement-y,0px))] data-ending-style:[transform:translateY(-100%)] data-starting-style:[transform:translateY(-100%)]",
left: "inset-y-0 left-0 w-3/4 sm:max-w-sm rounded-r-xl border-r-[length:var(--border-width)] [transform:translateX(var(--drawer-swipe-movement-x,0px))] data-ending-style:[transform:translateX(-100%)] data-starting-style:[transform:translateX(-100%)]",
right:
"inset-y-0 right-0 w-3/4 sm:max-w-sm rounded-l-xl border-l-[length:var(--border-width)] [transform:translateX(var(--drawer-swipe-movement-x,0px))] data-ending-style:[transform:translateX(100%)] data-starting-style:[transform:translateX(100%)]",
};
return (
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Viewport className="fixed inset-0 z-50">
<DrawerPrimitive.Popup
data-slot="drawer-content"
className={cn(
"group/drawer-content fixed z-50 flex flex-col bg-background text-sm text-foreground shadow-lg outline-none transition-transform duration-300 ease-(--ease-out-expo) data-swiping:transition-none data-ending-style:duration-[calc(var(--drawer-swipe-strength,0.5)*400ms)]",
positionClasses[side],
className,
)}
{...props}
>
{side === "bottom" && (
<div className="mx-auto mt-4 h-1 w-[100px] shrink-0 rounded-full bg-muted" />
)}
{children}
</DrawerPrimitive.Popup>
</DrawerPrimitive.Viewport>
</DrawerPrimitive.Portal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-0.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn(
"text-base font-medium tracking-tighter text-foreground",
className,
)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: DrawerPrimitive.Description.Props) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
DrawerTitle,
DrawerTrigger,
};Sheet
Non-swipeable side panel (simpler than Drawer). Built on Base UI's Dialog. Four sides: top, right, bottom, left.
Usage:
import {
Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle,
} from "@flow-industries/ui/components/sheet";
<Sheet>
<SheetTrigger render={<Button>Open sheet</Button>} />
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Menu</SheetTitle>
</SheetHeader>
<div>...</div>
</SheetContent>
</Sheet>Source:
"use client";
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "../../utils/cn";
import { Button } from "./button";
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left";
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding px-4 text-sm text-foreground shadow-lg transition duration-200 ease-(--ease-out-expo) data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t-[length:var(--border-width)] data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r-[length:var(--border-width)] data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l-[length:var(--border-width)] data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b-[length:var(--border-width)] data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
className="absolute top-3 right-3"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-0.5 py-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 py-4", className)}
{...props}
/>
);
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"text-base font-medium tracking-tighter text-foreground",
className,
)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
};Popover
Floating, non-modal. Exports: Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription.
Usage:
import { Popover, PopoverTrigger, PopoverContent } from "@flow-industries/ui/components/popover";
<Popover>
<PopoverTrigger render={<Button>More</Button>} />
<PopoverContent>Popover body</PopoverContent>
</Popover>Source:
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import type * as React from "react";
import { cn } from "../../utils/cn";
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
...props
}: PopoverPrimitive.Popup.Props &
Pick<
PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-background p-2.5 text-sm text-foreground shadow-md ring-1 ring-primary/10 outline-hidden duration-150 ease-(--ease-out) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-0.5 text-sm", className)}
{...props}
/>
);
}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return (
<PopoverPrimitive.Title
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
);
}
function PopoverDescription({
className,
...props
}: PopoverPrimitive.Description.Props) {
return (
<PopoverPrimitive.Description
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
}
export {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
};HoverCard
Hover-triggered preview using Base UI's PreviewCard. Exports: HoverCard, HoverCardTrigger, HoverCardContent.
Usage:
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@flow-industries/ui/components/hover-card";
<HoverCard>
<HoverCardTrigger render={<a href="#">@alice</a>} />
<HoverCardContent>
<strong>Alice</strong>
<p>Joined 2026</p>
</HoverCardContent>
</HoverCard>Source:
"use client";
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import { cn } from "../../utils/cn";
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
delay = 100,
...props
}: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger
data-slot="hover-card-trigger"
delay={delay}
{...props}
/>
);
}
function HoverCardContent({
className,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 4,
...props
}: PreviewCardPrimitive.Popup.Props &
Pick<
PreviewCardPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
className={cn(
"z-50 w-64 origin-(--transform-origin) rounded-lg bg-background p-2.5 text-sm text-foreground shadow-md ring-1 ring-primary/10 outline-hidden duration-150 ease-(--ease-out) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardContent, HoverCardTrigger };Tooltip
Label-only hint. Exports: Tooltip, TooltipTrigger, TooltipContent, TooltipProvider.
Usage:
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@flow-industries/ui/components/tooltip";
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<Button size="icon"><HelpIcon /></Button>} />
<TooltipContent>Help</TooltipContent>
</Tooltip>
</TooltipProvider>Source:
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "../../utils/cn";
function TooltipProvider({
delay = 0,
closeDelay = 150,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
closeDelay={closeDelay}
{...props}
/>
);
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };