For sighted users to preview content available behind a link.
import React from 'react';
import { styled, keyframes } from '@stitches/react';
import { mauve } from '@radix-ui/colors';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
const slideUpAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideRightAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(-2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const slideDownAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(-2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideLeftAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const StyledContent = styled(HoverCardPrimitive.Content, {
borderRadius: 6,
padding: 20,
width: 300,
backgroundColor: 'white',
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
animationFillMode: 'forwards',
willChange: 'transform, opacity',
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
});
const StyledArrow = styled(HoverCardPrimitive.Arrow, {
fill: 'white',
});
// Exports
export const HoverCard = HoverCardPrimitive.Root;
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
export const HoverCardContent = StyledContent;
export const HoverCardArrow = StyledArrow;
// Your app...
const Flex = styled('div', { display: 'flex' });
const ImageTrigger = styled('a', {
all: 'unset',
cursor: 'pointer',
borderRadius: '100%',
display: 'inline-block',
'&:focus': { boxShadow: `0 0 0 2px white` },
});
const Img = styled('img', {
display: 'block',
borderRadius: '100%',
variants: {
size: {
normal: { width: 45, height: 45 },
large: { width: 60, height: 60 },
},
},
defaultVariants: {
size: 'normal',
},
});
const Text = styled('div', {
margin: 0,
color: mauve.mauve12,
fontSize: 15,
lineHeight: 1.5,
variants: {
faded: {
true: { color: mauve.mauve10 },
},
bold: {
true: { fontWeight: 500 },
},
},
});
const HoverCardDemo = () => (
<HoverCard>
<HoverCardTrigger asChild>
<ImageTrigger href="https://twitter.com/radix_ui" target="_blank" rel="noreferrer noopener">
<Img src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png" />
</ImageTrigger>
</HoverCardTrigger>
<HoverCardContent sideOffset={5}>
<Flex css={{ flexDirection: 'column', gap: 7 }}>
<Img
size="large"
src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png"
/>
<Flex css={{ flexDirection: 'column', gap: 15 }}>
<Text>
<Text bold>Radix</Text>
<Text faded>@radix_ui</Text>
</Text>
<Text>
Components, icons, colors, and templates for building high-quality, accessible UI. Free
and open-source.
</Text>
<Flex css={{ gap: 15 }}>
<Flex css={{ gap: 5 }}>
<Text bold>0</Text> <Text faded>Following</Text>
</Flex>
<Flex css={{ gap: 5 }}>
<Text bold>2,900</Text> <Text faded>Followers</Text>
</Flex>
</Flex>
</Flex>
</Flex>
<HoverCardArrow />
</HoverCardContent>
</HoverCard>
);
export default HoverCardDemo;
Install the component from your command line.
npm install @radix-ui/react-hover-card
Import all parts and piece them together.
import * as HoverCard from '@radix-ui/react-hover-card';
export default () => (
<HoverCard.Root>
<HoverCard.Trigger />
<HoverCard.Content>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Root>
);
Contains all the parts of a hover card.
Prop | Type | Default |
---|---|---|
defaultOpen | boolean | |
open | boolean | |
onOpenChange | function | |
openDelay | number | 700 |
closeDelay | number | 300 |
The link that opens the hover card when hovered.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
The component that pops out when the hover card is open.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
forceMount | boolean | |
portalled | boolean | true |
side | enum | "bottom" |
sideOffset | number | 0 |
align | enum | "center" |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionTolerance | number | 0 |
An optional arrow element to render alongside the hover card. This can be used to help visually link the trigger with the HoverCard.Content
. Must be rendered inside HoverCard.Content
.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
width | number | 10 |
height | number | 5 |
offset | number |
Use the openDelay
prop to control the time it takes for the hover card to open.
import * as HoverCard from '@radix-ui/react-hover-card';
export default () => (
<HoverCard.Root delayDuration={0}>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCard.Content>…</HoverCard.Content>
</HoverCard.Root>
);
We expose a CSS custom property --radix-hover-card-content-transform-origin
. Use it to animate the content from its computed origin based on side
, sideOffset
, align
, alignOffset
and any collisions.
import { styled, keyframes } from '@stitches/react';
import * as HoverCard from '@radix-ui/react-hover-card';
const scaleIn = keyframes({
'0%': { opacity: 0, transform: 'scale(0)' },
'100%': { opacity: 1, transform: 'scale(1)' },
});
const HoverCardContent = styled(HoverCard.Content, {
transformOrigin: 'var(--radix-hover-card-content-transform-origin)',
animation: `${scaleIn} 0.5s ease-out forwards`,
});
export default () => (
<HoverCard.Root>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCardContent>…</HoverCardContent>
</HoverCard.Root>
);
We expose data-side
and data-align
attributes. Their values will change at runtime to reflect collisions. Use them to create collision and direction-aware animations.
import { styled, keyframes } from '@stitches/react';
import * as HoverCard from '@radix-ui/react-hover-card';
const slideDown = keyframes({
'0%': { opacity: 0, transform: 'translateY(-10px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideUp = keyframes({
'0%': { opacity: 0, transform: 'translateY(10px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const HoverCardContent = styled(HoverCard.Content, {
animationDuration: '0.6s',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
animationFillMode: 'forwards',
'&[data-side="top"]': { animationName: slideUp },
'&[data-side="bottom"]': { animationName: slideDown },
});
export default () => (
<HoverCard.Root>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCardContent>…</HoverCardContent>
</HoverCard.Root>
);
The hover card is intended for mouse users only so will not respond to keyboard navigation.