HoverCard

Display popover section when target element is hovered

Usage

import { HoverCard, Button, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard width={280} shadow="md">
        <HoverCard.Target>
          <Button>Hover to reveal the card</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">
            Hover card is revealed when user hovers over target element, it will be hidden once
            mouse is not over both target and dropdown elements
          </Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

Delays

Set open and close delays in ms with the openDelay and closeDelay props:

import { HoverCard, Button, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard shadow="md" openDelay={1000}>
        <HoverCard.Target>
          <Button>1000ms open delay</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">Opened with 1000ms delay</Text>
        </HoverCard.Dropdown>
      </HoverCard>

      <HoverCard shadow="md" closeDelay={1000}>
        <HoverCard.Target>
          <Button>1000ms close delay</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">Will close with 1000ms delay</Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

HoverCard delay group

Use the HoverCard.Group component to sync open and close delays of multiple HoverCard components:

import { HoverCard, Button, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <HoverCard.Group openDelay={500} closeDelay={100}>
      <Group justify="center">
        <HoverCard shadow="md">
          <HoverCard.Target>
            <Button>First</Button>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <Text size="sm">First hover card content</Text>
          </HoverCard.Dropdown>
        </HoverCard>

        <HoverCard shadow="md">
          <HoverCard.Target>
            <Button>Second</Button>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <Text size="sm">Second hover card content</Text>
          </HoverCard.Dropdown>
        </HoverCard>

        <HoverCard shadow="md">
          <HoverCard.Target>
            <Button>Third</Button>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <Text size="sm">Third hover card content</Text>
          </HoverCard.Dropdown>
        </HoverCard>
      </Group>
    </HoverCard.Group>
  );
}

Activation events

The events prop determines which interactions open the dropdown. Fields that are not specified keep their default values, for example events={{ focus: false }} disables focus activation and leaves hover activation enabled:

FieldDefaultDescription
hovertrueDropdown is opened when the target is hovered with a mouse
focustrueDropdown is opened when the target is focused with the keyboard
touchfalseDropdown is opened when the target is tapped on a touch device, requires hover to be enabled

Focus activation uses :focus-visible semantics: the dropdown is opened when the target is focused with the keyboard, but not when it is focused by a mouse click.

touch is disabled by default because there is no hover on touch devices – the first tap would open the dropdown instead of activating the target. Enable it only if the target does not have its own tap action. touch extends hover activation, it has no effect if hover is disabled.

import { HoverCard, Button, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard width={280} shadow="md" events={{ focus: false }}>
        <HoverCard.Target>
          <Button>Hover only</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">Focusing the target does not open this dropdown</Text>
        </HoverCard.Dropdown>
      </HoverCard>

      <HoverCard width={280} shadow="md" events={{ touch: true }}>
        <HoverCard.Target>
          <Button>Hover, focus and touch</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">This dropdown is also opened by a tap on touch devices</Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

Keyboard interactions

KeyDescription
TabMoves focus to the target, opens the dropdown if events.focus is enabled
EscapeCloses the dropdown, can be disabled with closeOnEscape={false}

The dropdown is also closed when the user presses outside of the target and the dropdown (pointerdown event), set closeOnClickOutside={false} to disable this behavior. Note that the clickOutsideEvents prop is not supported by HoverCard: outside press is always detected with the pointerdown event.

import { HoverCard, Button, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <Button variant="default">Press Tab to move focus</Button>

      <HoverCard width={280} shadow="md">
        <HoverCard.Target>
          <Button>Hover or focus me</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">
            The dropdown is opened when the target is hovered or focused with the keyboard.
            Press Escape to close it.
          </Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

Interactive dropdown

By default, the dropdown is closed as soon as the pointer leaves the target: if there is a gap between the target and the dropdown, or if the pointer travels diagonally over unrelated elements, the dropdown closes before the pointer reaches it.

Set the interactive prop to keep the dropdown open while the pointer travels toward it. It is required by WCAG 1.4.13 if the dropdown contains interactive content. Note that an interactive dropdown intercepts pointer events of the content it overlaps:

import { HoverCard, Button, Text, Group, Anchor } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard width={280} shadow="md" interactive position="bottom-end" offset={40}>
        <HoverCard.Target>
          <Button>Interactive</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">
            Move the pointer diagonally to the{' '}
            <Anchor href="https://mantine.dev" target="_blank">
              link
            </Anchor>{' '}
            – the dropdown stays open
          </Text>
        </HoverCard.Dropdown>
      </HoverCard>

      <HoverCard width={280} shadow="md" position="bottom-end" offset={40}>
        <HoverCard.Target>
          <Button variant="default">Not interactive</Button>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">Moving the pointer outside of the target closes this dropdown</Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

With interactive elements

HoverCard is displayed only when the mouse is over the target element or dropdown. You can use anchors and buttons within dropdowns, using inputs is not recommended:

import { HoverCard, Avatar, Text, Group, Anchor, Stack } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard width={320} shadow="md" withArrow openDelay={200} closeDelay={400}>
        <HoverCard.Target>
          <Avatar src="https://avatars.githubusercontent.com/u/79146003?s=200&v=4" radius="xl" />
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Group>
            <Avatar src="https://avatars.githubusercontent.com/u/79146003?s=200&v=4" radius="xl" />
            <Stack gap={5}>
              <Text size="sm" fw={700} style={{ lineHeight: 1 }}>
                Mantine
              </Text>
              <Anchor
                href="https://x.com/mantinedev"
                c="dimmed"
                size="xs"
                style={{ lineHeight: 1 }}
              >
                @mantinedev
              </Anchor>
            </Stack>
          </Group>

          <Text size="sm" mt="md">
            Customizable React components and hooks library with focus on usability, accessibility
            and developer experience
          </Text>

          <Group mt="md" gap="xl">
            <Text size="sm">
              <b>0</b> Following
            </Text>
            <Text size="sm">
              <b>1,174</b> Followers
            </Text>
          </Group>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

Dropdown role

The role prop controls the accessible relation between the target and the dropdown:

ValueTarget attributesDropdown attributes
dialog (default)aria-haspopup="dialog", aria-expanded, aria-controlsrole="dialog", aria-labelledby
tooltiparia-describedbyrole="tooltip"

Use role="tooltip" if the dropdown contains only descriptive content – its content is then announced by screen readers as the description of the target.

Keep the default role="dialog" if the dropdown contains headings, links, buttons or any other content that is not a plain description of the target.

Hydration

import { HoverCard, Text, Group } from '@mantine/core';

function Demo() {
  return (
    <Group justify="center">
      <HoverCard width={280} shadow="md" role="tooltip">
        <HoverCard.Target>
          <Text td="underline dotted" tabIndex={0} w="fit-content">
            Hydration
          </Text>
        </HoverCard.Target>
        <HoverCard.Dropdown>
          <Text size="sm">
            Attaching React event handlers to the HTML that was rendered on the server
          </Text>
        </HoverCard.Dropdown>
      </HoverCard>
    </Group>
  );
}

HoverCard.Target children

HoverCard.Target requires an element or a component as a single child – strings, fragments, numbers, and multiple elements/components are not supported and will throw an error. Custom components must provide a prop to get the root element ref; all Mantine components support ref out of the box.

import { HoverCard, Button } from '@mantine/core';

function Demo() {
  return (
    <>
      <HoverCard.Target>
        <button>Native button – ok</button>
      </HoverCard.Target>

      {/* OK */}
      <HoverCard.Target>
        <Button>Mantine component – ok</Button>
      </HoverCard.Target>

      {/* String, NOT OK – will throw error */}
      <HoverCard.Target>Raw string</HoverCard.Target>

      {/* Number, NOT OK – will throw error */}
      <HoverCard.Target>{2}</HoverCard.Target>

      {/* Fragment, NOT OK – will throw error */}
      <HoverCard.Target>
        <>Fragment, NOT OK, will throw error</>
      </HoverCard.Target>

      {/* Multiple nodes, NOT OK – will throw error */}
      <HoverCard.Target>
        <div>More that one node</div>
        <div>NOT OK, will throw error</div>
      </HoverCard.Target>
    </>
  );
}

Required ref prop

Custom components that are rendered inside HoverCard.Target are required to support the ref prop:

// Example of code that WILL NOT WORK
import { HoverCard } from '@mantine/core';

// ❌ ref is not forwarded to the root element
function MyComponent() {
  return <div>My component</div>;
}

// This will not work – MyComponent does not support ref
function Demo() {
  return (
    <HoverCard>
      <HoverCard.Target>
        <MyComponent />
      </HoverCard.Target>
    </HoverCard>
  );
}

Pass ref to the root element:

// Example of code that will work
import { HoverCard } from '@mantine/core';

// ✅ ref is forwarded to the root element
function MyComponent({ ref, ...others }: React.ComponentProps<'div'>) {
  return <div ref={ref} {...others}>My component</div>;
}

// Works correctly – ref is forwarded
function Demo() {
  return (
    <HoverCard>
      <HoverCard.Target>
        <MyComponent />
      </HoverCard.Target>
    </HoverCard>
  );
}

Accessibility

HoverCard can be opened with a mouse and with the keyboard, and is announced by screen readers:

  • The target is focusable if it is an interactive element (a button or a link). If the target is not interactive, add tabIndex={0} to it to make it reachable with the keyboard.
  • Focusing the target with the keyboard opens the dropdown, Escape and a press outside of the target and the dropdown close it.
  • onDismiss is called when the dropdown is closed with Escape key or with a press outside of the target and the dropdown.
  • aria-haspopup, aria-expanded, aria-controls and aria-describedby attributes are managed by the component, see the Dropdown role section.
  • The returnFocus prop is ignored while focus activation is enabled (events.focus, the default): focus is already on the target when the dropdown is opened with the keyboard, and returning it after the target was blurred would trap keyboard users on the target. Escape pressed inside the dropdown always returns focus to the target.

Limitations

The dropdown is rendered in a portal at the end of document.body, it is not a part of the tab order of the page. Keyboard users can open, read and dismiss the dropdown, but cannot move focus into it – links and buttons inside the dropdown are not reachable with the keyboard.

If the dropdown contains interactive content, that content must also be available elsewhere on the page. Do not use the trapFocus prop to work around this limitation: focus cannot be moved into the dropdown in the first place, and trapping focus in a hover-triggered element strands keyboard users.

With withinPortal={false} the dropdown is rendered next to the target and focus can be moved into it with Tab, but moving focus out of the dropdown with Tab does not close it – the dropdown is closed with Escape, with a press outside or when the pointer leaves the target.

Hover and focus activations do not coordinate: if the pointer leaves the target while the target is focused with the keyboard, the dropdown is closed after closeDelay and is not reopened until the target is blurred and focused again.