Popup
A popup displays brief content in an overlay.Motion in Early Access
Default
This is the simplest form of a popup. The popup opens from a trigger element.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const contentStyles = cssMap({
root: {
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
const PopupDefaultExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement="bottom-start"
content={() => <Box xcss={contentStyles.root}>Content</Box>}
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup{' '}
</Button>
)}
/>
);
};
export default PopupDefaultExample;Placement
Use the placement prop to set a preferred position (auto, top, right, left, or bottom).
The popup will move automatically if it's near the edge of the screen.
Using the auto placement will place the popup on the side with the most space available.
import { useState } from 'react';
import { css, jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { type Placement, placements } from '@atlaskit/popper/main';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const placementGridPositions = cssMap({
'top-start': {
gridColumn: 2,
gridRow: 1,
},
top: {
gridColumn: 3,
gridRow: 1,
},
'top-end': {
gridColumn: 4,
gridRow: 1,
},
'bottom-start': {
gridColumn: 2,
gridRow: 5,
},
bottom: {
gridColumn: 3,
gridRow: 5,
},
'bottom-end': {
gridColumn: 4,
gridRow: 5,
},
'right-start': {
gridColumn: 5,
gridRow: 2,
},
right: {
gridColumn: 5,
gridRow: 3,
},
'right-end': {
gridColumn: 5,
gridRow: 4,
},
'left-start': {
gridColumn: 1,
gridRow: 2,
},
left: {
gridColumn: 1,
gridRow: 3,
},
'left-end': {
gridColumn: 1,
gridRow: 4,
},
'auto-start': {
gridColumn: 3,
gridRow: 2,
},
auto: {
gridColumn: 3,
gridRow: 3,
},
'auto-end': {
gridColumn: 3,
gridRow: 4,
},
});
const contentStyles = css({
maxWidth: '220px',
paddingBlockEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingInlineStart: token('space.200'),
});
const buttonGridStyles = css({
display: 'grid',
gap: token('space.100'),
gridTemplate: 'repeat(5, 1fr) / repeat(5, 1fr)',
justifyItems: 'stretch',
});
const PopupPlacementExample = (): JSX.Element => {
const [openPlacement, setOpenPlacement] = useState<Placement | null>(null);
return (
<div css={buttonGridStyles}>
{placements.map((placement) => {
const isOpen = openPlacement === placement;
return (
<Popup
shouldRenderToParent
key={placement}
placement={placement}
isOpen={isOpen}
onClose={() => {
setOpenPlacement((value) => (value === placement ? null : value));
}}
content={() => (
<div css={contentStyles}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut aliquam massa ac risus
scelerisque, in iaculis magna semper. Phasellus sagittis congue elit, non suscipit
nulla rhoncus vitae.
</div>
)}
trigger={(triggerProps) => (
<Box xcss={placementGridPositions[placement]}>
<Button
{...triggerProps}
shouldFitContainer
isSelected={isOpen}
onClick={() => setOpenPlacement(isOpen ? null : placement)}
>
{placement}
</Button>
</Box>
)}
/>
);
})}
</div>
);
};
export default PopupPlacementExample;Multiple
You can use popups multiple times on the same page.
import { type FC, useState } from 'react';
import { jsx } from '@compiled/react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const contentStyles = cssMap({
root: {
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
type PopupExampleProps = {
index: number;
};
const PopupExample: FC<PopupExampleProps> = ({ index }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={() => <Box xcss={contentStyles.root}>Content</Box>}
trigger={(triggerProps) => (
<Button {...triggerProps} isSelected={isOpen} onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close' : 'Open'} popup {index + 1}
</Button>
)}
placement="bottom-start"
/>
);
};
const PopupMultipleExample = (): JSX.Element => (
<ButtonGroup label="Open required popup">
{Array.from(Array(3)).map((_, index) => (
<PopupExample index={index} />
))}
</ButtonGroup>
);
export default PopupMultipleExample;Nested
Nesting popups inside other popups works out of the box. However, be mindful that nested popups quickly become confusing, inaccessible, and difficult to navigate. If you’re nesting a lot of content, it’s time to move your experience to a new page instead.
This example has additional code to prevent the parent popup closing when interacting with the child popup.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import ArrowRight from '@atlaskit/icon/core/arrow-right';
import MenuIcon from '@atlaskit/icon/core/menu';
import ButtonItem from '@atlaskit/menu/button-item';
import Section from '@atlaskit/menu/section';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
const nestedPopupStyles = cssMap({
root: {
maxWidth: '800px',
minWidth: '320px',
},
});
const NestedPopup = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<Box onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Stack xcss={nestedPopupStyles.root}>
<Section title="Projects">
<ButtonItem>Create project</ButtonItem>
<ButtonItem>View all projects</ButtonItem>
</Section>
<Section hasSeparator>
<Popup
isOpen={isOpen}
placement="right-start"
shouldRenderToParent
onClose={() => setIsOpen(false)}
content={() => <NestedPopup />}
trigger={(triggerProps) => (
<ButtonItem
{...triggerProps}
isSelected={isOpen}
onClick={() => setIsOpen(true)}
iconAfter={<ArrowRight label="" />}
>
More actions
</ButtonItem>
)}
/>
</Section>
</Stack>
</Box>
);
};
const PopupNestedExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={() => <NestedPopup />}
placement="bottom-start"
shouldRenderToParent
trigger={(triggerProps) => (
<Button
{...triggerProps}
isSelected={isOpen}
iconBefore={MenuIcon}
onClick={() => setIsOpen(!isOpen)}
>
Menu
</Button>
)}
/>
);
};
export default PopupNestedExample;Content updates
When changing the content inside the popup, call the update() function to reposition the popup.
Make sure to call this in the same callback or update step to ensure everything happens in the same animation frame.
import { useEffect, useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import noop from '@atlaskit/ds-lib/noop';
import Heading from '@atlaskit/heading/heading';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { Stack } from '@atlaskit/primitives/compiled/stack';
import { Text } from '@atlaskit/primitives/compiled/text';
import Toggle from '@atlaskit/toggle/toggle';
import { token } from '@atlaskit/tokens';
import { data } from './data';
const wrapperStyles = cssMap({
root: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
});
const contentStyles = cssMap({
root: {
maxWidth: '350px',
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
textAlign: 'center',
},
});
const Values = ({ onUpdate }: { onUpdate: () => void }) => {
const [textIndex, setTextIndex] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setTextIndex((prevIndex) => (prevIndex + 1) % data.length);
onUpdate();
}, 15000);
return () => clearInterval(intervalId);
}, [onUpdate]);
return (
<Box xcss={contentStyles.root} aria-live="assertive" aria-atomic="true">
<Stack space="space.100">
<Heading size="large">{data[textIndex].title}</Heading>
<Box as="blockquote">
<Text>{data[textIndex].description}</Text>
</Box>
</Stack>
</Box>
);
};
const PopupContentUpdateExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
const [isUpdateOn, setIsUpdateOn] = useState(true);
return (
<Box xcss={wrapperStyles.root}>
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={(props) => <Values onUpdate={isUpdateOn ? props.update : noop} />}
placement="right"
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
Open popup
</Button>
)}
/>
<Box>
<Text>Updates {isUpdateOn ? 'on' : 'off'}</Text>
<Toggle
size="large"
label="Updates toggle switch controls"
isChecked={isUpdateOn}
onChange={(e) => setIsUpdateOn(e.currentTarget.checked)}
/>
</Box>
</Box>
);
};
export default PopupContentUpdateExample;Customization
You can customize the popup using a component override with the popupComponent prop.
Make sure to forward the ref to the underlying DOM node, (opens new window), or the positioning of the popup won't work correctly.
import { forwardRef, useState } from 'react';
import { css, jsx } from '@compiled/react';
import IconButton from '@atlaskit/button/icon/button';
import ShowMoreHorizontalIcon from '@atlaskit/icon/core/show-more-horizontal';
import { Popup } from '@atlaskit/popup/popup';
import type { PopupComponentProps } from '@atlaskit/popup/types';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const containerStyles = css({
zIndex: 400,
backgroundColor: token('color.background.brand.bold'),
borderRadius: token('radius.small'),
color: token('color.text.inverse'),
paddingBlockEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingInlineStart: token('space.200'),
});
const CustomPopupContainer: React.ForwardRefExoticComponent<
React.PropsWithoutRef<PopupComponentProps> & React.RefAttributes<HTMLDivElement>
> = forwardRef<HTMLDivElement, PopupComponentProps>(
({ children, 'data-testid': testId, xcss: _xcss, ...props }, ref) => (
<div
css={containerStyles}
test-id={testId}
{...props}
className={props.className}
ref={ref}
>
{children}
</div>
),
);
const PopupCustomExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
shouldRenderToParent
isOpen={isOpen}
testId="popup-custom-example"
onClose={() => setIsOpen(false)}
placement="bottom-start"
popupComponent={CustomPopupContainer}
content={() => <Box>Customized popup</Box>}
trigger={(triggerProps) => (
<IconButton
{...triggerProps}
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
icon={ShowMoreHorizontalIcon}
label="More"
/>
)}
/>
);
};
export default PopupCustomExample;Focus
Set focus
You can focus a specific element when opening the popup. The content render props provide a ref
setInitialFocusRef to be set on the desired element. Only set focus to an element at the start of
the popup, otherwise people who use assistive technology may miss the content.
The specified element needs to be available in the document as soon as the popup is opened, otherwise it will not be focused upon. This means it will not work for content that renders into the popup content after it has already opened, such as async or lazy loaded content. In those cases, you will need to manually set focus on the content once it is rendered.
Select a button to focus
import { Fragment, useState } from 'react';
import { jsx } from '@compiled/react';
import ButtonGroup from '@atlaskit/button/button-group';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import RadioGroup from '@atlaskit/radio/radio-group';
import { token } from '@atlaskit/tokens';
const radioValues = [
{ name: 'None', value: '0', label: 'None' },
{ name: 'Button 1', value: '1', label: 'Button 1' },
{ name: 'Button 2', value: '2', label: 'Button 2' },
{ name: 'Button 3', value: '3', label: 'Button 3' },
];
const contentStyles = cssMap({
root: {
paddingBlockEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingInlineStart: token('space.200'),
},
});
const PopupFocusExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
const [buttonToFocus, setButtonToFocus] = useState('0');
return (
<Fragment>
<p>
<strong>Select a button to focus</strong>
</p>
<RadioGroup
onChange={({ currentTarget: { value } }) => setButtonToFocus(value)}
defaultValue={radioValues[0].value}
options={radioValues}
/>
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={({ setInitialFocusRef }) => {
return (
<Box xcss={contentStyles.root}>
<ButtonGroup label="Show selected focus">
{radioValues.map(
({ value, label }) =>
value !== '0' && (
<Button
key={value}
ref={value === buttonToFocus ? setInitialFocusRef : undefined}
>
{label}
</Button>
),
)}
</ButtonGroup>
</Box>
);
}}
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup
</Button>
)}
placement="bottom-start"
/>
</Fragment>
);
};
export default PopupFocusExample;Disable autofocus
If an element is focused, setting the autofocus prop to false will ensure it keeps focus when
the popup is opened.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import Textfield from '@atlaskit/textfield/text-field';
import { token } from '@atlaskit/tokens';
const wrapperStyles = cssMap({
root: {
display: 'grid',
alignItems: 'center',
gap: token('space.200'),
gridTemplateColumns: '1fr auto',
},
});
const contentStyles = cssMap({
root: {
maxWidth: '200px',
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
const PopupDisableAutofocusExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Box xcss={wrapperStyles.root}>
{/* eslint-disable-next-line @atlaskit/design-system/no-placeholder */}
<Textfield placeholder="This should stay focused when the popup opens" />
<Popup
shouldRenderToParent
autoFocus={false}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={() => (
<Box xcss={contentStyles.root}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut aliquam massa ac risus
scelerisque, in iaculis magna semper. Phasellus sagittis congue elit, non suscipit nulla
rhoncus vitae.
</Box>
)}
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup
</Button>
)}
placement="bottom"
/>
</Box>
);
};
export default PopupDisableAutofocusExample;Surface detection
The current surface CSS variable is set to the
surface color of the popup. You can use the utility.elevation.surface.current design token to
style children with the current surface color.
import { useState } from 'react';
import { css, jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { Popup } from '@atlaskit/popup/popup';
import { Flex } from '@atlaskit/primitives/compiled/flex';
import { token } from '@atlaskit/tokens';
const contentStyles = css({
maxWidth: '220px',
backgroundColor: token('utility.elevation.surface.current'),
paddingBlockEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingInlineStart: token('space.200'),
});
const SurfaceAwareBox = () => {
return (
<div css={contentStyles}>
A surface aware box - the background color depends on the surface it's placed on.
</div>
);
};
const PopupSurfaceDetectionExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Flex gap="space.200">
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement="bottom-start"
content={SurfaceAwareBox}
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup
</Button>
)}
/>
<SurfaceAwareBox />
</Flex>
);
};
export default PopupSurfaceDetectionExample;Content wrapping
By default, the popup content is rendered inside React.Portal. Use shouldRenderToParent prop to
render the content directly after the trigger element.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const contentStyles = cssMap({
root: {
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
const PopupContentWithoutPortalExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement="bottom-start"
content={() => <Box xcss={contentStyles.root}>Content</Box>}
shouldRenderToParent
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup{' '}
</Button>
)}
/>
);
};
export default PopupContentWithoutPortalExample;Role
Use the role prop to set a role for the popup content. We do not forbid passing any of the aria
roles, but we strongly recommend using only menu or dialog. When the role="dialog" property is
passed, one of the following properties must also be added: label or titleId.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const contentStyles = cssMap({
root: {
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
const PopupWithRoleExample = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
shouldRenderToParent
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement="bottom-start"
content={() => <Box xcss={contentStyles.root}>Content</Box>}
role="dialog"
label="Popup with role dialog example"
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? 'Close' : 'Open'} popup{' '}
</Button>
)}
/>
);
};
export default PopupWithRoleExample;Full width
Use shouldFitContainer to fit the popup width to its parent's width. When set to true, the
trigger and popup elements will be wrapped in a div with position: relative. The popup will be
rendered as a sibling to the trigger element, and will be full width.
import { useState } from 'react';
import { jsx } from '@compiled/react';
import Button from '@atlaskit/button/default/button';
import { cssMap } from '@atlaskit/css';
import { Popup } from '@atlaskit/popup/popup';
import { Box } from '@atlaskit/primitives/compiled/box';
import { token } from '@atlaskit/tokens';
const contentStyles = cssMap({
root: {
paddingInlineStart: token('space.200'),
paddingInlineEnd: token('space.200'),
paddingBlockStart: token('space.200'),
paddingBlockEnd: token('space.200'),
},
});
const PopupFullWidth = (): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
<Popup
shouldRenderToParent
content={() => <Box xcss={contentStyles.root}>Content</Box>}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
placement="bottom-start"
shouldFitContainer
trigger={(triggerProps) => (
<Button
{...triggerProps}
appearance="primary"
isSelected={isOpen}
onClick={() => setIsOpen(!isOpen)}
shouldFitContainer
>
{isOpen ? 'Close' : 'Open'} popup{' '}
</Button>
)}
/>
);
};
export default PopupFullWidth;