Inkbox

Component

Popover

A raised white panel with 8px corners and the floating shadow that grows out of the control that opened it.

npx -y github:anwarlemu/inkbox-brand-cli#release add popover
Preview
Open

Usage

Use for a small form or detail tied to one control (rename a mailbox, set a phone line's state). Put a PopoverHeader with a PopoverTitle and PopoverDescription first, then the fields. For choosing one value from a list use Select or Combobox; for actions use Dropdown Menu.

Avoid

  • A popover that opens on hover (use Hover Card)
  • A dark or lilac popover panel
  • Scaling from the center instead of the trigger
  • Long forms that scroll inside a popover

Exports: Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger. This component is a client component.

Dependencies: surface, radix-ui.

Example

examples/popover.tsx
"use client";​import * as React from "react";import { PencilSimpleIcon } from "@phosphor-icons/react/ssr";​import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";​function RenameForm({ onDone }: { onDone?: () => void }) {  const [name, setName] = React.useState("Acme support");  return (    <form      className="grid gap-3"      onSubmit={(event) => {        event.preventDefault();        onDone?.();      }}    >      <PopoverHeader>        <PopoverTitle>Display name</PopoverTitle>        <PopoverDescription>Shown as the sender on acme@inkboxmail.com.</PopoverDescription>      </PopoverHeader>      <div className="grid gap-1">        <Label htmlFor="display-name">Name</Label>        <Input id="display-name" value={name} onChange={(e) => setName(e.target.value)} />      </div>      <Button type="submit" className="w-full">        Save      </Button>    </form>  );}​export function Preview() {  const [open, setOpen] = React.useState(false);  return (    <div className="flex justify-center">      <Popover open={open} onOpenChange={setOpen}>        <PopoverTrigger asChild>          <Button variant="outline">            <PencilSimpleIcon weight="fill" />            Rename mailbox          </Button>        </PopoverTrigger>        <PopoverContent align="start">          <RenameForm onDone={() => setOpen(false)} />        </PopoverContent>      </Popover>    </div>  );}​export function Open() {  return (    <div className="flex min-h-80 items-start justify-center">      <Popover defaultOpen>        <PopoverTrigger asChild>          <Button variant="outline">            <PencilSimpleIcon weight="fill" />            Rename mailbox          </Button>        </PopoverTrigger>        <PopoverContent>          <RenameForm />        </PopoverContent>      </Popover>    </div>  );}

Source

components/ui/popover.tsx
"use client";​import * as React from "react";import { Popover as PopoverPrimitive } from "radix-ui";​import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";​// A popover, extrapolated from the console's raised white panels (Figma 427:2715: the select and the selected tab// carry Shadow/S): white with 8px corners and the floating shadow (Shadow/S's ring with a softer, longer drop), 16px of// padding, 14px ink text. It grows out of its trigger from 96 percent with a fade (200ms, the strong ease-out) and// leaves faster than it came.function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>) {  return <PopoverPrimitive.Root data-slot="popover" {...props} />;}​function PopoverTrigger(props: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;}​function PopoverAnchor(props: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;}​function PopoverContent({ className, align = "center", sideOffset = 6, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) {  const container = usePortalContainer();  return (    <PopoverPrimitive.Portal container={container}>      <PopoverPrimitive.Content        data-slot="popover-content"        align={align}        sideOffset={sideOffset}        className={cn(          "z-50 flex w-72 max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) flex-col gap-3 rounded-control bg-surface p-4 text-body leading-body text-fg shadow-floating outline-none data-[state=closed]:animate-pop-out data-[state=open]:animate-pop-in",          className,        )}        {...props}      />    </PopoverPrimitive.Portal>  );}​/** The popover's head: its title over a short muted line. */function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {  return <div data-slot="popover-header" className={cn("flex flex-col gap-0.5", className)} {...props} />;}​function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {  return <h2 data-slot="popover-title" className={cn("font-sans text-body-lg leading-body font-medium text-fg", className)} {...props} />;}​function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {  return <p data-slot="popover-description" className={cn("text-body leading-body text-fg-muted", className)} {...props} />;}​export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };

Reference: Extrapolated from the Inkbox console (Figma 427:2715): the raised white panel of the select and the selected tab (Shadow/S), the field and button sizes.