GSAP in Webflow · Recipes

Build an accessible dialog or slide-in panel

A dialog is one GSAP timeline played forward to open and reversed to close, plus four non-negotiable ARIA attributes, real button elements, an outside-click overlay stacked behind the panel, and a scroll lock — and it is not finished until focus is managed.

  • intermediate
  • Last verified: July 27, 2026
  • Verified against: cloneable

The rule

One timeline. Open plays it, close reverses it. Do not build a closing animation.

The panel wrapper is position: fixed, full-viewport, high z-index, display: none, height: 100dvh. The open interaction is three or four actions:

AtActionDoes
0ssetwrapper displayflex
0sanimatewrapper opacity — type from, 0%
0sanimatepanel move Y (or move X) — type from, e.g. 2rem / 100%
0ssetscroll lock — see Lock page scroll while an overlay is open

Then on the close control: a click event with control reverse. That’s the entire close.

Four ARIA attributes, none optional:

role="dialog"          → announces it as a dialog window, not a generic container
aria-modal="true"      → tells assistive tech the background is not interactive
aria-labelledby="…"    → points at the heading's id, so it announces its own title
tabindex="-1"          → focusable by script, but not in the tab order

Controls must be real button elements — a Webflow custom element with the tag button and type="button". Webflow’s native button renders as an <a>, i.e. navigation. A real button is focusable, responds to Enter and Space, and is announced as an action. Icon-only controls need an aria-label (close dialog, open join newsletter dialog), and decorative icons inside them need aria-hidden="true".

Outside-click, done right

Do not put the close trigger on the wrapper — it would fire on every click, including inside the form.

Add an overlay div that fills the wrapper but sits behind the panel. Both have z-index: auto, so stacking follows DOM order: drag the overlay above the panel in the navigator to push it behind visually. Clicks inside the panel now never reach the overlay.

Then give the overlay and the close button the same attribute and target that, so one trigger serves both:

  • popup build: dialog-animation="close"
  • CMS sidebar build: data-sidebar-close

Variant A — one dialog on the page

Target by class or a custom selector. Close by reverse. This is the simple modal: newsletter signup, confirmation, video lightbox.

Variant B — one panel per CMS item

Put the panel inside the collection item, so each card renders its own, bound to that item’s fields. There is no shared panel and no id-matching script.

One interaction serves every card, using the target/filter/reference composition (Target filters — resolving a target relative to the trigger):

ElementTargetFilterReference
the panel, on openAttribute data-animate="sidebar"Previous sibling ofTrigger element
overlay / content wrapperAttributeWithinParent of trigger
the panel, on closeAttributeContainsTrigger element

aria-labelledby here points at the heading’s id set from the CMS slug, so each item’s dialog announces its own title rather than all of them sharing one.

Close is not a reverse in this build, because the scroll lock must outlive the exit: fade the overlay out, slide the content out, then at start 0.8s set display: none and remove the lock class.

Choosing between them

SituationVariant
One dialog, fixed contentA
A panel per card in a collectionB
Close is a plain reverseA
The lock must survive the exit animationB

It is not done yet

A dialog that animates and locks scroll is still not accessible: a keyboard user can Tab straight out of it into the page behind. Add the focus trap — see Trap keyboard focus inside an open panel. That is what makes it behave like a real modal.

Currency note — native <dialog> (checked 2026-07-27)

The browser now does much of this for free. A native <dialog> opened with showModal() traps focus, returns focus to the opener on close, closes on Escape, and gives you a ::backdrop. The closedby="any" attribute adds click-outside dismissal (Chrome 134+, part of Interop 2026).

But it does not replace this recipe for an IX3-driven panel, for one concrete reason: showModal() is a JavaScript call, and a GSAP interaction cannot make it. The moment you drive opening from the interactions panel, you own the semantics and the focus management yourself — which is exactly what the ARIA set and Trap keyboard focus inside an open panel above provide.

Tell a reader building with custom code to prefer native <dialog>. Tell a reader building visually in Webflow to use this recipe.

Verify

Publish. The Designer canvas will not exercise scroll lock or focus. On the live link: open and confirm the page behind does not scroll; click outside and confirm it closes; press Escape; then Tab all the way round and confirm focus never leaves the panel and returns to the control that opened it.

Sources