--- meta: title: Popup description: 'Popup is a utility that lets you declaratively anchor "popup" containers to another element.' layout: component --- This component's name is inspired by [``](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/Popup/explainer.md). It uses [Floating UI](https://floating-ui.com/) under the hood to provide a well-tested, lightweight, and fully declarative positioning utility for tooltips, dropdowns, and more. Popup doesn't provide any styles — just positioning! The popup's preferred placement, distance, and skidding (offset) can be configured using attributes. An arrow that points to the anchor can be shown and customized to your liking. Additional positioning options are available and described in more detail below. :::warning Popup is a low-level utility built specifically for positioning elements. Do not mistake it for a [tooltip](/components/tooltip) or similar because _it does not facilitate an accessible experience!_ Almost every correct usage of `` will involve building other components. It should rarely, if ever, occur directly in your HTML. ::: ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlSelect from '@shoelace-style/shoelace/dist/react/select'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; import SlSwitch from '@shoelace-style/shoelace/dist/react/switch'; const css = ` .popup-overview sl-popup { --arrow-color: var(--sl-color-primary-600); } .popup-overview span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-overview .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-overview-options { display: flex; flex-wrap: wrap; align-items: end; gap: 1rem; } .popup-overview-options sl-select { width: 160px; } .popup-overview-options sl-input { width: 100px; } .popup-overview-options + .popup-overview-options { margin-top: 1rem; } `; const App = () => { const [placement, setPlacement] = useState('top'); const [distance, setDistance] = useState(0); const [skidding, setSkidding] = useState(0); const [active, setActive] = useState(true); const [arrow, setArrow] = useState(false); return ( <>
setPlacement(event.target.value)} > top top-start top-end bottom bottom-start bottom-end right right-start right-end left left-start left-end setDistance(event.target.value)} /> setSkidding(event.target.value)} />
setActive(event.target.checked)}> Active setArrow(event.target.checked)}> Arrow
); }; ``` :::tip A popup's anchor should not be styled with `display: contents` since the coordinates will not be eligible for calculation. However, if the anchor is a `` element, popup will use the first assigned element as the anchor. This behavior allows other components to pass anchors through more easily via composition. ::: ## Examples ### Activating Popups are inactive and hidden until the `active` attribute is applied. Removing the attribute will tear down all positioning logic and listeners, meaning you can have many idle popups on the page without affecting performance. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlSwitch from '@shoelace-style/shoelace/dist/react/switch'; const css = ` .popup-active span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-active .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } `; const App = () => { const [active, setActive] = useState(true); return ( <>

setActive(event.target.checked)}> Active
); }; ``` ### External Anchors By default, anchors are slotted into the popup using the `anchor` slot. If your anchor needs to live outside of the popup, you can pass the anchor's `id` to the `anchor` attribute. Alternatively, you can pass an element reference to the `anchor` property to achieve the same effect without using an `id`. ```html:preview
``` ```jsx:react import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; const css = ` #external-anchor { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px 0 0 50px; } #external-anchor ~ sl-popup .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } `; const App = () => { return ( <>
); }; ``` ### Placement Use the `placement` attribute to tell the popup the preferred placement of the popup. Note that the actual position will vary to ensure the panel remains in the viewport if you're using positioning features such as `flip` and `shift`. Since placement is preferred when using `flip`, you can observe the popup's current placement when it's active by looking at the `data-current-placement` attribute. This attribute will update as the popup flips to find available space and it will be removed when the popup is deactivated. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlSelect from '@shoelace-style/shoelace/dist/react/select'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const css = ` .popup-placement span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-placement .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-placement sl-select { max-width: 280px; } `; const App = () => { const [placement, setPlacement] = useState('top'); return (
setPlacement(event.target.value)}> top top-start top-end bottom bottom-start bottom-end right right-start right-end left left-start left-end
); }; ``` ### Distance Use the `distance` attribute to change the distance between the popup and its anchor. A positive value will move the popup further away and a negative value will move it closer. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlRange from '@shoelace-style/shoelace/dist/react/range'; const css = ` .popup-distance span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-distance .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-distance sl-range { max-width: 260px; } `; const App = () => { const [distance, setDistance] = useState(0); return ( <>
setDistance(event.target.value)} />
); }; ``` ### Skidding The `skidding` attribute is similar to `distance`, but instead allows you to offset the popup along the anchor's axis. Both positive and negative values are allowed. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlRange from '@shoelace-style/shoelace/dist/react/range'; const css = ` .popup-skidding span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-skidding .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-skidding sl-range { max-width: 260px; } `; const App = () => { const [skidding, setSkidding] = useState(0); return ( <>
setSkidding(event.target.value)} />
); }; ``` ### Arrows Add an arrow to your popup with the `arrow` attribute. It's usually a good idea to set a `distance` to make room for the arrow. To adjust the arrow's color and size, use the `--arrow-color` and `--arrow-size` custom properties, respectively. You can also target the `arrow` part to add additional styles such as shadows and borders. By default, the arrow will be aligned as close to the center of the _anchor_ as possible, considering available space and `arrow-padding`. You can use the `arrow-placement` attribute to force the arrow to align to the start, end, or center of the _popup_ instead. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlSelect from '@shoelace-style/shoelace/dist/react/select'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; import SlSwitch from '@shoelace-style/shoelace/dist/react/switch'; const css = ` .popup-arrow sl-popup { --arrow-color: var(--sl-color-primary-600); } .popup-arrow span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-arrow .box { width: 100px; height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-arrow-options { display: flex; flex-wrap: wrap; align-items: end; gap: 1rem; } .popup-arrow-options sl-select { width: 160px; } .popup-arrow-options + .popup-arrow-options { margin-top: 1rem; } `; const App = () => { const [placement, setPlacement] = useState('top'); const [arrowPlacement, setArrowPlacement] = useState('anchor'); const [arrow, setArrow] = useState(true); return ( <>
setPlacement(event.target.value)} > top top-start top-end bottom bottom-start bottom-end right right-start right-end left left-start left-end setArrowPlacement(event.target.value)} > anchor start end center
setArrow(event.target.checked)}> Arrow
); }; ``` ### Syncing with the Anchor's Dimensions Use the `sync` attribute to make the popup the same width or height as the anchor element. This is useful for controls that need the popup to stay the same width or height as the trigger. ```html:preview ``` ```jsx:react import { useState } from 'react'; import SlPopup from '@shoelace-style/shoelace/dist/react/popup'; import SlSelect from '@shoelace-style/shoelace/dist/react/select'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const css = ` .popup-sync span[slot='anchor'] { display: inline-block; width: 150px; height: 150px; border: dashed 2px var(--sl-color-neutral-600); margin: 50px; } .popup-sync .box { width: 100%; height: 100%; min-width: 50px; min-height: 50px; background: var(--sl-color-primary-600); border-radius: var(--sl-border-radius-medium); } .popup-sync sl-switch { margin-top: 1rem; } `; const App = () => { const [sync, setSync] = useState('width'); return ( <>