Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <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>
              
            
!

CSS

              
                .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);
}
              
            
!

JS

              
                '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)
})
              
            
!
999px

Console