ephemera

Popover

A slip of paper with a folded corner.

Install

$ npx shadcn@latest add https://ephemera.kerrshift.com/r/popover.json

Or copy the source below into components/ui/popover.tsx. Core components need the Paper Theme and Radix where noted.

Usage

example.tsx
import { Popover, PopoverTrigger, PopoverContent, PopoverClose, PopoverAnchor } from "@/components/ui/popover";

<Popover>
  <PopoverTrigger asChild><Button variant="paper">Address</Button></PopoverTrigger>
  <PopoverContent>
    <Label>Street</Label>
    <Input defaultValue="221B Baker St" />
  </PopoverContent>
</Popover>

Built on the Radix Popover, so keyboard navigation and screen reader support come for free.

Props

PropTypeDefaultDescription
PopoverContent side / align / sideOffsetRadix positioningside "bottom", align "center", 8Where the slip appears.
open / onOpenChangeboolean—Controlled state.

Any other props are passed to the root element.

Fonts

Components read optional CSS variables and fall back to system fonts: --font-display, --font-receipt, --font-type, --font-hand and --font-stamp. This site uses Fraunces, IBM Plex Mono, Courier Prime, Caveat and Anton.

Source

components/ui/popover.tsx
"use client";

import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui";

function usePaperContainer() {
  const anchor = React.useRef<HTMLSpanElement>(null);
  const [container, setContainer] = React.useState<HTMLElement | undefined>();
  React.useEffect(() => {
    const el = anchor.current?.closest<HTMLElement>("[data-paper]");
    setContainer(el && el !== document.documentElement ? el : undefined);
  }, []);
  return [anchor, container] as const;
}

/** A slip of paper with a folded corner. */
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverAnchor = PopoverPrimitive.Anchor;
export const PopoverClose = PopoverPrimitive.Close;

const FOLD = 16;

export function PopoverContent({
  className = "",
  align = "center",
  sideOffset = 8,
  children,
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
  const [anchor, container] = usePaperContainer();
  return (
    <>
      <span ref={anchor} hidden />
      <PopoverPrimitive.Portal container={container}>
        <PopoverPrimitive.Content
          align={align}
          sideOffset={sideOffset}
          data-paper-motion
          className="z-50 origin-(--radix-popover-content-transform-origin) outline-none drop-shadow-[0_12px_18px_rgb(0_0_0/.25)] data-[state=open]:animate-[paper-pop-in_.16s_ease-out]"
          {...props}
        >
          <div
            className={`paper relative w-72 bg-(--paper-raised) p-5 text-(--ink) ${className}`}
            style={{ clipPath: `polygon(0 0, calc(100% - ${FOLD}px) 0, 100% ${FOLD}px, 100% 100%, 0 100%)` }}
          >
            {/* the folded-down corner */}
            <span
              aria-hidden
              className="absolute right-0 top-0"
              style={{
                width: FOLD,
                height: FOLD,
                background: "linear-gradient(225deg, transparent 50%, color-mix(in srgb, var(--paper-sunk) 80%, #000) 50%)",
              }}
            />
            {children}
          </div>
        </PopoverPrimitive.Content>
      </PopoverPrimitive.Portal>
    </>
  );
}