Skip to content
Development
Agent

lightbox-patterns

<!-- Loaded by react-portfolio-engineer when task involves lightbox, image viewer, or modal gallery -->

From plugin
vexjoy-agent
449198 skills198 agents12 commands83 hooks

Claude Code

$ npx -y skills add notque/vexjoy-agent --agent claude-code

Agent definition

lightbox-patterns.md

Lightbox Patterns Reference

<!-- Loaded by react-portfolio-engineer when task involves lightbox, image viewer, or modal gallery -->

Keyboard Navigation

Every lightbox needs keyboard nav (accessibility + power-user feature). Handle Escape, ArrowLeft/Right, Home/End.

// hooks/useLightboxKeyboard.ts
import { useEffect } from 'react'

export function useLightboxKeyboard({ onClose, onPrev, onNext, onFirst, onLast }) {
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      switch (e.key) {
        case 'Escape':     onClose(); break
        case 'ArrowLeft':  onPrev();  break
        case 'ArrowRight': onNext();  break
        case 'Home':       onFirst?.(); break
        case 'End':        onLast?.(); break
      }
    }
    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [onClose, onPrev, onNext, onFirst, onLast])
}

---

Focus Trap

Focus must not escape to background content while overlay is open.

// hooks/useFocusTrap.ts
export function useFocusTrap(active: boolean): React.RefObject<HTMLDivElement> {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!active || !containerRef.current) return
    const focusable = containerRef.current.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last = focusable[focusable.length - 1]
    first?.focus()

    function handleKeyDown(e: KeyboardEvent) {
      if (e.key !== 'Tab') return
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus() }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus() }
    }
    document.addEventListener('keydown', handleKeyDown)
    return () => document.removeEventListener('keydown', handleKeyDown)
  }, [active])

  return containerRef
}

---

Scroll Lock

Prevent background scrolling. Compensate for scrollbar width to avoid layout shift.

// hooks/useScrollLock.ts
export function useScrollLock(locked: boolean) {
  useEffect(() => {
    if (!locked) return
    const originalOverflow = document.body.style.overflow
    const originalPadding = document.body.style.paddingRight
    const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
    document.body.style.overflow = 'hidden'
    document.body.style.paddingRight = `${scrollbarWidth}px`
    return () => {
      document.body.style.overflow = originalOverflow
      document.body.style.paddingRight = originalPadding
    }
  }, [locked])
}

---

Lightbox Component (Lazy Loading Adjacent Images)

'use client'
import Image from 'next/image'

export function Lightbox({ images, activeIndex, onClose, onNext, onPrev }: LightboxProps) {
  const containerRef = useFocusTrap(true)
  useScrollLock(true)
  useLightboxKeyboard({ onClose, onPrev, onNext })

  const current = images[activeIndex]
  const prev = images[activeIndex - 1]
  const next = images[activeIndex + 1]

  return (
    <div ref={containerRef} role="dialog" aria-modal="true"
      aria-label={`Viewing: ${current.alt}`}
      className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
      onClick={onClose}>
      <div className="relative max-w-5xl w-full mx-4" onClick={e => e.stopPropagation()}>
        <Image src={current.src} alt={current.alt} width={current.width} height={current.height}
          priority className="w-full h-auto object-contain max-h-[85vh]" />
      </div>

      {prev && <Image src={prev.src} alt="" width={1} height={1} className="sr-only" loading="eager" />}
      {next && <Image src={next.src} alt="" width={1} height={1} className="sr-only" loading="eager" />}

      <button onClick={onPrev} disabled={activeIndex === 0} aria-label="Previous image"
        className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl disabled:opacity-30">Prev</button>
      <button onClick={onNext} disabled={activeIndex === images.length - 1} aria-label="Next image"
        className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl disabled:opacity-30">Next</button>
      <button onClick={onClose} aria-label="Close image viewer"
        className="absolute top-4 right-4 text-white text-2xl">Close</button>

      <p className="absolute bottom-4 left-1/2 -translate-x-1/2 text-white text-sm">
        {activeIndex + 1} / {images.length}
      </p>
    </div>
  )
}

---

Swipe Gestures (Mobile)

// hooks/useSwipeGesture.ts
export function useSwipeGesture(onSwipeLeft: () => void, onSwipeRight: () => void, threshold = 50) {
  const startX = useRef<number>(0)
  return {
    onTouchStart(e: React.TouchEvent) { startX.current = e.touches[0].clientX },
    onTouchEnd(e: React.TouchEvent) {
      const delta = e.changedTouches[0].clientX - startX.current
      if (Math.abs(delta) < threshold) return
      if (delta < 0) onSwipeLeft(); else onSwipeRight()
    },
  }
}

Attach: `<div {...useSwipeGesture(onNext, onPrev)} className="fixed inset-0 ...">`

---

Gallery Grid Layout

export function GalleryGrid({ images, onImageClick }: GalleryGridProps) {
  return (
    <ul role="list" className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2" aria-label="Image gallery">
      {images.map((image, index) => (
        <li key={image.id}>
          <button onClick={() => onImageClick(index)} aria-label={`View full size: ${image.alt}`}
            className="relative block w-full aspect-square overflow-hidden rounded focus:ring-2">
            <Image src={image.src} alt={image.alt} fill className="object-cover"
              sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw"
              loading={index < 4 ? 'eager' : 'lazy'} />
          </button>
        </li>
      ))}
    </ul>
  )
}

Use `<button>` not `<div onClick>` — keyboard nav and screen reader acc

Read more
@notque
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

44964 skills198 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
449
Star history of notque/vexjoy-agent on GitHub, a chart by star-history.com

Other agents on vexjoy-agent.

modules

modules

Agent

**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes…

@notque@notqueInstall Agent
testing

testing

Agent

**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**:…

@notque@notqueInstall Agent
communication-patterns

communication-patterns

Agent

**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what…

@notque@notqueInstall Agent

More agents from other plugins.

a11y-architect
Auto-invoked

a11y-architect

Agent

Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI…

ecc13d293k
@affaan-m@affaan-mInstall Agent
analyze-pass-impact

analyze-pass-impact

Agent

Analyzes how a specific topic affects a group of compiler passes. Used by the /plan-update skill to parallelize research across…

react2d252k
@facebook@facebookInstall Agent
orchestrator

orchestrator

Agent

- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as…

codex2d135k
@openai@openaiInstall Agent
cavecrew-builder

cavecrew-builder

Agent

Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical renames, comment removal, format-preserving tweaks. Hard…

caveman3d119k
@juliusbrussee@juliusbrusseeInstall Agent

Questions about lightbox-patterns.

Which coding agents can use lightbox-patterns?
lightbox-patterns installs in Claude Code and Cursor: each loads subagents from .claude/agents, so copy the file there. Flowy's auto-invocation runs in Claude Code only.