easeul

No libraryTransitions

Native popover

A popover is not a dialog: nothing behind it is blocked, and the browser dismisses it on a click outside. The exit still plays while the element is in the top layer, which is the part a naive implementation gets wrong.

Every value the manifest allows on this host, single presets and the pairs that compose. The first is what this component ships with. Both stages are the same component, so anything that changes between them is the attribute.

withoutno attribute

Not modal, and light dismissed

Nothing behind this is blocked: scroll the page or press another button while it is open. Click outside or press Escape and the browser closes it, and the exit plays while the element is still in the top layer.

with easefuldata-motion="scale-fade"

Not modal, and light dismissed

Nothing behind this is blocked: scroll the page or press another button while it is open. Click outside or press Escape and the browser closes it, and the exit plays while the element is still in the top layer.

host writes

:popover-open, and the top layer

easeful matches

[data-motion~="scale-fade"][popover]:not(:popover-open)

what runs

transition: opacity, scale, display, overlay

you write

<div popover data-motion="scale-fade">

Where the attribute goes

The parts this demo composes, and the value each one carries. Everything inside an animated part is that part's content, so the tree stops there.

  1. button
  2. divdata-motion="scale-fade"

Source

"use client"

import { useId } from "react"
import type { DemoProps } from "@/components/demos/props"

export function NativePopover({ motion }: DemoProps) {
  const id = useId()
  /* One anchor name per instance. The page mounts this demo twice, and two triggers sharing
     a name would both resolve to whichever comes last in the DOM, so the control column's
     popover would jump to the other column's button. useId is not a valid ident, hence the
     scrub. */
  const anchor = { "--anchor": `--pop-${id.replace(/[^a-z0-9]/gi, "")}` } as React.CSSProperties

  return (
    <>
      <button type="button" className="btn pop__trigger" popoverTarget={id} style={anchor}>
        Open popover
      </button>
      <div id={id} popover="auto" className="pop" data-motion={motion} style={anchor}>
        <p className="panel__title">Not modal, and light dismissed</p>
        <p className="panel__body">
          Nothing behind this is blocked: scroll the page or press another button while it is
          open. Click outside or press Escape and the browser closes it, and the exit plays
          while the element is still in the top layer.
        </p>
        <div className="modal__actions">
          <button
            type="button"
            className="btn btn--primary"
            popoverTarget={id}
            popoverTargetAction="hide"
          >
            Close
          </button>
        </div>
      </div>
    </>
  )
}
The whole file, read from disk at build time. Both copies above render from it, and the motion prop is the only difference between them.