Clawscale

Popover

PopoverNext shows floating content next to a target: menus, small forms and details. It positions with Floating UI and moves to stay on screen.

Usage#

import { PopoverNext } from "@clawscale/react";
<PopoverNext content={<Menu>{/* items */}</Menu>} placement="bottom-start">
  <Button endIcon="caret-down" text="Export" />
</PopoverNext>

Pass the target as the only child and the floating content as content. PopoverNext wraps the target in a span. To render the target yourself, without a wrapper, use renderTarget and spread the props it receives onto your element.

Examples#

Basic#

A click on the target toggles the popover. Classes.POPOVER_CONTENT_SIZING pads the content. A click on an element with Classes.POPOVER_DISMISS closes it.

import { Button, Classes, H6, PopoverNext } from "@clawscale/react";

export default function PopoverBasic() {
  return (
    <PopoverNext
      placement="bottom"
      popoverClassName={Classes.POPOVER_CONTENT_SIZING}
      content={
        <div>
          <H6>Pause ingest-orders?</H6>
          <p>Scheduled runs wait until you resume. 2 runs are queued.</p>
          <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
            <Button className={Classes.POPOVER_DISMISS} text="Cancel" />
            <Button className={Classes.POPOVER_DISMISS} intent="warning" text="Pause" />
          </div>
        </div>
      }
    >
      <Button icon="pause" text="Pause pipeline" />
    </PopoverNext>
  );
}

Placement#

placement sets the side, optionally with a -start or -end alignment. The popover flips when that side runs out of room. Leave placement unset to pick the side with the most room.

import { Button, Classes, POPOVER_NEXT_PLACEMENTS, PopoverNext } from "@clawscale/react";

export default function PopoverPlacement() {
  return (
    <div
      style={{
        display: "grid",
        gap: 8,
        gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
        width: "100%",
        maxWidth: 480,
      }}
    >
      {POPOVER_NEXT_PLACEMENTS.map((placement) => (
        <PopoverNext
          content={`placement="${placement}"`}
          fill
          key={placement}
          placement={placement}
          popoverClassName={Classes.POPOVER_CONTENT_SIZING}
        >
          <Button text={placement} />
        </PopoverNext>
      ))}
    </div>
  );
}

Interaction kinds#

interactionKind sets what opens and closes the popover.

  • click (default): a click on the target opens it. A click anywhere else closes it.
  • click-target: only clicks on the target open and close it.
  • hover: opens on hover and stays open while the pointer is over the target or the popover.
  • hover-target: opens on hover and closes when the pointer leaves the target.
import { Button, Menu, MenuItem, PopoverNext } from "@clawscale/react";

const kinds = ["click", "click-target", "hover", "hover-target"] as const;

export default function PopoverInteraction() {
  const menu = (
    <Menu>
      <MenuItem icon="play" shouldDismissPopover={false} text="Run now" />
      <MenuItem icon="pause" shouldDismissPopover={false} text="Pause schedule" />
      <MenuItem icon="history" shouldDismissPopover={false} text="Run history" />
    </Menu>
  );
  return (
    <>
      {kinds.map((kind) => (
        <PopoverNext content={menu} interactionKind={kind} key={kind} placement="bottom">
          <Button endIcon="caret-down" text={kind} />
        </PopoverNext>
      ))}
    </>
  );
}

Controlled#

Pass isOpen to control the popover. onInteraction reports the state the user asked for. Here submitting the form closes it.

import { Button, Classes, InputGroup, PopoverNext } from "@clawscale/react";
import { useState } from "react";

export default function PopoverControlled() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <PopoverNext
      isOpen={isOpen}
      onInteraction={(nextOpen) => setIsOpen(nextOpen)}
      placement="bottom"
      popoverClassName={Classes.POPOVER_CONTENT_SIZING}
      content={
        <form
          style={{ display: "grid", gap: 12, width: 240 }}
          onSubmit={(event) => {
            event.preventDefault();
            setIsOpen(false);
          }}
        >
          <InputGroup aria-label="Dataset name" defaultValue="orders_daily" />
          <Button intent="primary" text="Rename dataset" type="submit" />
        </form>
      }
    >
      <Button icon="edit" text="Rename" />
    </PopoverNext>
  );
}

Minimal#

arrow={false} removes the arrow and animation="minimal" makes the transition subtle. Use both for popovers that open often, such as menus.

import { Button, Classes, PopoverNext } from "@clawscale/react";

const freshness = "orders_daily was refreshed 4 min ago.";

export default function PopoverMinimal() {
  return (
    <>
      <PopoverNext content={freshness} placement="bottom" popoverClassName={Classes.POPOVER_CONTENT_SIZING}>
        <Button text="Default" />
      </PopoverNext>
      <PopoverNext
        animation="minimal"
        arrow={false}
        content={freshness}
        placement="bottom"
        popoverClassName={Classes.POPOVER_CONTENT_SIZING}
      >
        <Button text="Minimal" />
      </PopoverNext>
    </>
  );
}

Put a Menu in a minimal popover. A click on a menu item closes the popover, unless the item sets shouldDismissPopover={false}.

import { Button, Menu, MenuDivider, MenuItem, PopoverNext } from "@clawscale/react";

export default function PopoverDropdown() {
  return (
    <PopoverNext
      animation="minimal"
      arrow={false}
      placement="bottom-start"
      content={
        <Menu>
          <MenuItem icon="export" text="Export CSV" />
          <MenuItem icon="export" text="Export Parquet" />
          <MenuItem icon="clipboard" text="Copy as SQL" />
          <MenuDivider />
          <MenuItem icon="share" text="Share view" />
        </Menu>
      }
    >
      <Button endIcon="caret-down" icon="export" text="Export" />
    </PopoverNext>
  );
}

Match target width#

matchTargetWidth makes the popover as wide as its target. fill stretches the target to its container.

import { Button, Menu, MenuItem, PopoverNext } from "@clawscale/react";
import { useState } from "react";

const regions = ["us-east-1", "us-west-2", "eu-west-1", "ap-south-1"];

export default function PopoverMatchTargetWidth() {
  const [region, setRegion] = useState("eu-west-1");
  return (
    <div style={{ width: "100%", maxWidth: 280 }}>
      <PopoverNext
        animation="minimal"
        arrow={false}
        fill
        matchTargetWidth
        placement="bottom-start"
        content={
          <Menu>
            {regions.map((name) => (
              <MenuItem active={name === region} key={name} text={name} onClick={() => setRegion(name)} />
            ))}
          </Menu>
        }
      >
        <Button alignText="start" endIcon="caret-down" icon="globe" text={region} />
      </PopoverNext>
    </div>
  );
}

Interactive playground#

Props
Open dropdown
Open dropdown
View source on GitHub

Props interface#

interface PopoverNextProps@clawscale/react

Props interface for PopoverNext component.

PropsDescription
animation
PopoverAnimation = "scale"
The animation style to use for the popover.
arrow
boolean = true
Whether to show the arrow pointing to the target.
autoFocus
boolean = true for click interactions, false for hover interactions
Whether the popover/tooltip should acquire application focus when it first opens.
autoUpdateOptions
PopoverNextAutoUpdateOptions
Options forwarded to floating-ui's autoUpdate to configure how the popover position is automatically updated while it is open. By default, all autoUpdate behaviors are enabled (ancestorScroll, ancestorResize, elementResize, and layoutShift).
backdropProps
React.HTMLProps<HTMLDivElement>
HTML props for the backdrop element. Can be combined with backdropClassName.
boundary
PopoverNextBoundary
A boundary element supplied to the positioning middleware. This is a shorthand for overriding Floating UI middleware options with the middleware prop.
canEscapeKeyClose
boolean
Whether the popover can be closed by pressing the Escape key.
captureDismiss
boolean = false
When enabled, clicks inside a Classes.POPOVER_DISMISS element will only close the current popover and not outer popovers. When disabled, the current popover and any ancestor popovers will be closed.
children
React.ReactNode
Interactive element which will trigger the popover.
className
string
A space-delimited string of class names applied to the popover's target wrapper element.
content
string | React.JSX.Element
The content displayed inside the popover.
defaultIsOpen
boolean = false
Initial opened state when uncontrolled.
disabled
boolean = false
Prevents the popover from appearing when true.
enforceFocus
boolean
Whether the popover should enforce focus within itself.
fill
boolean
Whether the wrapper and target should take up the full width of their container. Note that supplying true for this prop will force targetTagName="div".
hasBackdrop
boolean = false
Enables an invisible overlay beneath the popover that captures clicks and prevents interaction with the rest of the document until the popover is closed. This prop is only available when interactionKind is PopoverInteractionKind.CLICK. When popovers with backdrop are opened, they become focused.
hoverCloseDelay
number = 300
The amount of time in milliseconds the popover should remain open after the user hovers off the trigger. The timer is canceled if the user mouses over the target before it expires.
hoverOpenDelay
number = 150
The amount of time in milliseconds the popover should wait before opening after the user hovers over the trigger. The timer is canceled if the user mouses away from the target before it expires.
inheritDarkTheme
boolean = true
Whether a popover that uses a Portal should automatically inherit the dark theme from its parent.
interactionKind
PopoverInteractionKind = "click"
The kind of interaction that triggers the display of the popover.
isOpen
boolean = undefined
Whether the popover is visible. Passing this prop puts the popover in controlled mode, where the only way to change visibility is by updating this property. If disabled={true}, this prop will be ignored, and the popover will remain closed.
lazy
boolean
Whether the popover should be rendered lazily.
matchTargetWidth
boolean = false
Whether the popover content should be sized to match the width of the target. This is sometimes useful for dropdown menus.
middleware
MiddlewareConfig
Config for Floating UI middlewares. Each config is a partial options object, keyed by its middleware name.
onClose
(event?: React.SyntheticEvent<HTMLElement>) => void
Callback invoked when the popover is closed.
onClosed
(node: HTMLElement) => void
Callback invoked when the popover has finished closing.
onClosing
(node: HTMLElement) => void
Callback invoked when the popover is closing.
onInteraction
(nextOpenState: boolean, e?: React.SyntheticEvent<HTMLElement>) => void
Callback invoked in controlled mode when the popover open state would change due to user interaction.
onOpened
(node: HTMLElement) => void
Callback invoked when the popover has finished opening.
onOpening
(node: HTMLElement) => void
Callback invoked when the popover is opening.
openOnTargetFocus
boolean = true
Whether the popover should open when its target is focused. If true, target will render with tabindex="0" to make it focusable via keyboard navigation.
placement
PopoverNextPlacement
The placement (relative to the target) at which the popover should appear. Mutually exclusive with position prop. Prefer using this over position, as it more closely aligns with Floating UI semantics.
popoverClassName
string
A space-delimited string of class names applied to the popover element.
popoverRef
React.Ref<HTMLDivElement>
DOM ref attached to the Classes.POPOVER element.
popupKind
PopupKind = "menu" or undefined
The kind of popup displayed by the popover. Gets directly applied to the aria-haspopup attribute of the target element. This property is ignored if interactionKind is PopoverInteractionKind.HOVER_TARGET_ONLY.
portalClassName
string
A space-delimited string of class names applied to the portal element.
portalContainer
HTMLElement
The container element into which the popover content is rendered.
positioningStrategy
PopoverNextPositioningStrategy = "absolute"
The CSS position strategy for the floating popover element.
renderTarget
(props: PopoverRenderTargetProps & PopoverHoverTargetHandlers<T> & PopoverClickTargetHandlers<T>) => React.JSX.Element
Target renderer which receives props injected by Popover which should be spread onto the rendered element. This function should return a single React node.
rootBoundary
PopoverNextRootBoundary
A root boundary element supplied to the positioning middleware. This is a shorthand for overriding Floating UI middleware options with the middleware prop.
shouldReturnFocusOnClose
boolean = true
Whether the application should return focus to the last active element in the document after this popover closes.
targetProps
T
HTML props for the target element. This is useful in some cases where you need to render some simple attributes on the generated target element.
targetTagName
keyof React.JSX.IntrinsicElements = "span" ("div" if `fill={true}`)
HTML tag name for the target element. This must be an HTML element to ensure that it supports the necessary DOM event handlers.
transitionDuration
number
The duration of the popover's transition in milliseconds.
usePortal
boolean = true
Whether the popover should be rendered inside a Portal attached to portalContainer prop.

Legacy Popover#

Blueprint deprecated Popover in favor of PopoverNext. It positions content with Popper.js and places it in the wrong spot under React 19. Blueprint's own Tooltip, MenuItem and Select already use PopoverNext.

To migrate, rename these props:

PopoverPopoverNext
position="bottom-left"placement="bottom-start"
minimalarrow={false} and animation="minimal"
modifiersmiddleware

shouldReturnFocusOnClose now defaults to true. popoverPropsToNextProps converts a legacy props object.