<p>
  Click a link to show the target image.
  <br>Use the arrow keys to cycle through a category.
  <br>Hit Esc or click somewhere next to the image to hide it.
</p>
<dl>
  <dt>Sports</dt>
  <dd><a href="http://lorempixel.com/400/200/sports/1/" data-tightbox="sports">First</a></dd>
  <dd><a href="http://lorempixel.com/500/200/sports/2/" data-tightbox="sports">Second</a></dd>
  <dd><a href="http://lorempixel.com/400/300/sports/3/" data-tightbox="sports">Third</a></dd>
  <dt>Technics</dt>
  <dd><a href="http://lorempixel.com/400/200/technics/1/" data-tightbox="technics">First</a></dd>
  <dd><a href="http://lorempixel.com/500/200/technics/2/" data-tightbox="technics">Second</a></dd>
  <dd><a href="http://lorempixel.com/400/300/technics/3/" data-tightbox="technics">Third</a></dd>
</dl>
.tightbox-overlay {
  display: none;
  position: fixed;
  background-color: rgba(0, 0, 0, .5);
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  opacity: 0;
}

.tightbox-container {
  position: absolute;
  top: 50%;
  left: 50%;
  border: 5px solid white;
  border-radius: 3px;
}

.tightbox-image {
  display: block;
}

.tightbox-button {
  display: none;
  position: absolute;
  top: 0;
  bottom: 0;
  width: 0;
  height: 0;
  top: 50%;
  margin-top: -2em;
}

.tightbox-button-left {
  left: 2em;
  border-bottom: 2em solid transparent;
  border-top: 2em solid transparent;
  border-right: 4em solid rgba(255, 255, 255, .5);
}

.tightbox-button-right {
  right: 2em;
  border-bottom: 2em solid transparent;
  border-top: 2em solid transparent;
  border-left: 4em solid rgba(255, 255, 255, .5);
}
View Compiled
'use strict'

/////////////////////////////////////////////////////////////////////////////
// This is a image-popup demo to demonstrate some techniques to manipulate //
// the DOM and handle events. Some things (such as the fade-effect) would  //
// better be done using CSS in real life, though.                          //
// MIT @ m3g4p0p                                                           //
/////////////////////////////////////////////////////////////////////////////

///////////////
// Constants //
///////////////

/**
 * Style constants
 * @type {Object}
 */
const STYLE = {
  SHOW: 'block',
  HIDE: 'none'
}

/**
 * Class constants
 */
const CLASS = {
  OVERLAY: 'tightbox-overlay',
  CONTAINER: 'tightbox-container',
  IMAGE: 'tightbox-image',
  BUTTON: {
    DEFAULT: 'tightbox-button',
    LEFT: 'tightbox-button-left',
    RIGHT: 'tightbox-button-right'
  }
}

/**
 * Selector constants
 * @type {Object}
 */
const SELECTOR = {
  TIGHTBOX: '[data-tightbox]'
}

/**
 * Key constants
 * @type {Object}
 */
const KEY = {
  ESC: 27,
  LEFT: 37,
  RIGHT: 39
}

/**
 * Time constants
 * @type {Object}
 */
const TIME = {
  FADE: {
    DEFAULT: 300,
    SHORT: 100
  },
  THROTTLE: 100
}

///////////////////
// HTML elements //
///////////////////

/**
 * Links to trigger the tightbox
 * @type {Nodelist}
 */
const links = document.querySelectorAll(SELECTOR.TIGHTBOX)

/**
 * Overlay element
 * @type {HTMLElement}
 */
const overlay = document.createElement('div')

/**
 * Container element
 * @type {HTMLElement}
 */
const container = document.createElement('div')

/**
 * Image element
 * @type {HTMLElement}
 */
const image = document.createElement('img')

/**
 * Buttom left element
 * @type {HTMLElement}
 */
const buttonLeft = document.createElement('a')

/**
 * Button right element
 * @type {HTMLElement}
 */
const buttonRight = document.createElement('a')

/**
 * Groups of links to cycle through, as defined by the data-tightbox
 * attribute
 * @type {Object}
 */
const groups = Array.from(links).reduce((carry, link) => {
  const group = link.dataset.tightbox

  carry[group] = carry[group] || []
  carry[group].push(link)
  return carry
}, {})

/**
 * Reference to the currently shown image
 * @type {HTMLElement|undefined}
 */
let current

///////////////
// Functions //
///////////////

/**
 * Thunk for binding an event listener once
 * @param  {HTMLElement} element
 * @param  {String} event
 * @param  {Function} callback
 */
const once = (element, event, callback) => {
  element.addEventListener(event, callback, {
    once: true
  })
}

/**
 * Function to throttle the execution of event handlers
 * @param  {Function} callback
 * @param  {Number|undefined} delay
 * @return {Function}
 */
const throttle = (callback, delay) => {
  let hold = false
  delay = delay || TIME.THROTTLE

  return function handleEvent (...args) {
    if (hold) return

    hold = true
    callback.apply(this, args)

    window.setTimeout(() => {
      hold = false
    }, delay)
  }
}

/**
 * Function to fade an element
 * @param  {HTMLElement} element
 * @param  {Number} from
 * @param  {Number} to
 * @param  {Number} [duration=300]
 */
const fade = (element, from, to, duration) => {
  const start = window.performance.now()

  if (from === -1) {
    from = 1 * window
      .getComputedStyle(element)
      .getPropertyValue('opacity')
  }

  duration = duration || TIME.FADE.DEFAULT
  element.style.display = STYLE.SHOW

  window.requestAnimationFrame(function step (timestamp) {
    const progress = timestamp - start
    element.style.opacity = from + (progress / duration) * (to - from)

    if (progress < duration) {
      window.requestAnimationFrame(step)
    } else if (element.style.opacity <= 0) {
      element.style.display = STYLE.HIDE
    }
  })
}

/**
 * Fade in shorthand
 * @param  {HTMLElement} element
 * @param  {Number|undefined} duration
 */
const fadeIn = (element, duration) => {
  fade(element, -1, 1, duration)
}

/**
 * Fade out shorthand
 * @param  {HTMLElement} element
 * @param  {Number|undefined} duration
 */
const fadeOut = (element, duration) => {
  fade(element, -1, 0, duration)
}

/**
 * Function to center a (visible) element to its
 * top/left coordinates
 * @param  {HTMLElement} element
 */
const center = element => {
  Object.assign(element.style, {
    marginLeft: element.offsetWidth / -2 + 'px',
    marginTop: element.offsetHeight / -2 + 'px'
  })
}

/**
 * Hide the tightbox
 * @param  {Event} event
 */
const hideBox = event => {
  fadeOut(overlay)
  document.removeEventListener('keydown', keyHandler)  
  event.preventDefault()    
}

/**
 * Show the previous image in the current group
 * @param  {Event} event
 */
const showPrev = event => {
  const group = groups[current.dataset.tightbox]
  const index = group.indexOf(current)
  const prev = index === 0 ? group.length - 1 : index - 1

  current = group[prev]
  image.src = current.href
  once(image, 'load', () => center(container))
  event.preventDefault()    
}

/**
 * Show the next image in the current group
 * @param  {Event} event
 */
const showNext = event => {
  const group = groups[current.dataset.tightbox]
  const index = group.indexOf(current)
  const next = index === group.length - 1 ? 0 : index + 1

  current = group[next]
  image.src = current.href
  once(image, 'load', () => center(container))  
  event.preventDefault()    
}

/**
 * Callback for the 'keydown' event
 * @param  {Event} event
 */
const keyHandler = event => {
  const group = groups[current.dataset.tightbox]
  const index = group.indexOf(current)

  switch (event.which) {
    case KEY.ESC: {
      hideBox(event)
      break
    }

    case KEY.LEFT: {
      showPrev(event)
      break
    }

    case KEY.RIGHT: {
      showNext(event)
    }
  }
}

////////////////////
// Get it running //
////////////////////

// Initialize the tightbox
buttonLeft.href = buttonRight.href = '#'

overlay.classList.add(CLASS.OVERLAY)
container.classList.add(CLASS.CONTAINER)
image.classList.add(CLASS.IMAGE)
buttonLeft.classList.add(CLASS.BUTTON.DEFAULT, CLASS.BUTTON.LEFT)
buttonRight.classList.add(CLASS.BUTTON.DEFAULT, CLASS.BUTTON.RIGHT)

container.appendChild(image)
container.appendChild(buttonLeft)
container.appendChild(buttonRight)
overlay.appendChild(container)
document.body.appendChild(overlay)

// Hide the tightbox when clicking somewhere on the overlay
overlay.addEventListener('click', event => {
  if (event.target !== overlay) return

  hideBox(event)
})

// Navigate forward/backward in the current group
buttonLeft.addEventListener('click', showPrev)
buttonRight.addEventListener('click', showNext)

// Show the tightbox when clicking a corresponding link
document.addEventListener('click', event => {
  const {target} = event

  if (!target.matches(SELECTOR.TIGHTBOX)) return

  event.preventDefault()
  current = target
  image.src = current.href

  once(image, 'load', () => {
    document.addEventListener('keydown', keyHandler)
    fadeIn(overlay)
    center(container)
  })
})

// Show the navigation buttons when hovering the image
image.addEventListener('mousemove', throttle(event => {
  if (event.offsetX < event.target.offsetWidth / 2) {
    fadeIn(buttonLeft, TIME.FADE.SHORT)
    fadeOut(buttonRight, TIME.FADE.SHORT)
  } else {
    fadeOut(buttonLeft, TIME.FADE.SHORT)
    fadeIn(buttonRight, TIME.FADE.SHORT)
  }
}))

// Hide the navigation buttons when hovering the overlay
// (but not one of its children)
overlay.addEventListener('mouseover', event => {
  if (event.target !== overlay) return

  fadeOut(buttonLeft, TIME.FADE.SHORT)
  fadeOut(buttonRight, TIME.FADE.SHORT)
})
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.