Popover
Anchored popover behavior with focus restoration and outside-click dismissal.
Popover content stays app-owned.
import { css, on, type Handle } from 'remix/ui'
import button from 'remix/ui/button'
import * as popover from 'remix/ui/popover'
export default function Example(handle: Handle) {
let open = false
function setOpen(nextOpen: boolean) {
open = nextOpen
void handle.update()
}
return () => (
<popover.Context>
<div mix={demoCss}>
<button
mix={[
button(),
popover.anchor({ placement: 'bottom-start', offset: 8 }),
popover.focusOnHide(),
on('click', () => {
setOpen(!open)
}),
]}
>
View options
</button>
<div
mix={[
surfaceCss,
popover.surface({
open,
onHide() {
setOpen(false)
},
}),
]}
>
<button
mix={[
button({ tone: 'ghost' }),
popover.focusOnShow(),
on('click', () => {
setOpen(false)
}),
]}
>
Close
</button>
<div mix={surfaceBodyCss}>Popover content stays app-owned.</div>
</div>
</div>
</popover.Context>
)
}
const demoCss = css({
display: 'grid',
placeItems: 'center',
minHeight: '10rem',
width: 'min(100%, 24rem)',
})
const surfaceCss = css({
boxSizing: 'border-box',
width: '14rem',
margin: 0,
border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '8px',
background: '#FFFFFF',
boxShadow: '0 16px 40px rgba(0, 0, 0, 0.14)',
color: '#101010',
padding: '8px',
'&:popover-open': {
display: 'grid',
gap: '8px',
},
})
const surfaceBodyCss = css({
fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
fontSize: '13px',
lineHeight: '18px',
fontWeight: 500,
letterSpacing: 0,
color: '#4f4f4f',
padding: '2px 4px 4px',
})