This lesson builds a sidebar that does far more than slide in from the edge. Each card in a CMS grid opens its own sidebar with extra detail, the page scroll locks while it’s open, you can dismiss it by clicking outside, and — the part most tutorials skip — it’s genuinely accessible: proper dialog semantics and a real focus trap for keyboard and screen-reader users.
The interesting techniques are two: using GSAP’s relationship-based target filters so one interaction serves every card, and layering the accessibility that turns a pretty panel into a proper dialog.
How it works
One sidebar per CMS item. Because the sidebar lives inside the collection item, every card renders its own panel bound to that item’s fields — thumbnail, title, summary, sidebar image. Click a card and the matching sidebar is already right there. The sidebar wrapper is position: fixed, full-viewport, high z-index, display: none by default; inside it sit a semi-transparent overlay (position: absolute, full) and a content wrapper pushed right (margin-left: auto, ~50% width, max-width, overflow: auto so tall content stays scrollable).
Attribute-driven targeting with filters. Since there are many identical sidebars, the interaction can’t target by class — it has to find this card’s sidebar. The trick is custom attributes (data-animate, data-sidebar-open, data-sidebar-close) plus GSAP’s target filters, which target relative to the trigger: previous sibling of trigger (the sidebar next to the open button), within parent of trigger (the overlay/content inside the shared item), and contains trigger (the sidebar that wraps whichever close control was clicked). One interaction, correct target every time.
Open, close, and scroll lock. The open interaction is a set action flipping the sidebar’s display to flex (0s), an animate fading the overlay in (from opacity 0), an animate sliding the content in (from move-X 100%), and a set action adding an overflow-hidden class to the body to lock scroll. The close interaction is the reverse: fade the overlay out, slide the content out, then — at start 0.8s, once the animation finishes — set display back to none and remove the scroll-lock class. Both the close button and the overlay carry data-sidebar-close, so a single close interaction handles button-click and click-outside.
Real accessibility, not decoration. Every element that triggers an action is a real button (custom element, type="button") with an aria-label, and decorative icons get aria-hidden="true". The sidebar itself gets role="dialog", aria-modal="true", and aria-labelledby pointing at the heading’s id — set from the CMS slug so each item’s dialog announces its own title. Finally tabindex="-1" makes the panel focusable by script without adding it to the tab order.
The focus trap. A dialog isn’t accessible until focus is managed. A small script (in Before </body>) remembers which card opened the panel, moves focus into the sidebar (to the close button) on open, traps Tab so it cycles through the panel’s controls instead of the page behind it, closes on Escape, and returns focus to the originating card on close. That’s what makes it behave like a true modal dialog for keyboard users.
How to use it
-
Structure (inside the CMS item).
sidebardiv:position: fixed, full, z-index ~3000,display: none. Inside: an overlay (position: absolute, full, black 50%) and asidebar_content-wrapper(margin-left: auto, ~50% width, max-width,overflow: auto,position: relative). Bind image/title/summary to CMS fields. -
Tag everything.
data-animateon the sidebar, overlay, and content wrapper (distinct values);data-sidebar-openon the card’s open button;data-sidebar-closeon both the close button and the overlay. Make the open/close/overlay controls realbuttoncustom elements witharia-labels. -
Open interaction. set display flex (target sidebar via previous sibling of trigger), animate overlay opacity from 0, animate content move-X from 100%, set
overflow-hiddenon body — all at 0s, 0.8s, ease in-out expo. -
Close interaction. Trigger on
data-sidebar-close. Reverse the animations (overlay to 0, content to 100%), then at start 0.8s set display none (target via contains trigger) and remove theoverflow-hiddenclass. -
Accessibility. On the sidebar:
role="dialog",aria-modal="true",aria-labelledby=<slug>(and set the heading’sidto the same slug),tabindex="-1". Paste the focus-trap script into *Before</body>*. Publish and test with the keyboard (Tab, Escape) on the live link.
The focus-trap / keyboard script comes from the cloneable — copy it from there rather than reconstructing it, and confirm it matches your attribute names.