Responsive gallery with:
- CSS grid
- PhotoSwipe
- Lazysizes
- Pug, Sass & CoffeeScript
A Pen by Michal Niewitala 🍋 on CodePen.
| //- PREVIOUS VERSION: https://codepen.io/mican/pen/awxmpY | |
| - var placeholder = function(width, height) { return "data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20viewBox%3D'0%200%20" + width + "%20" + height + "'%20%2F%3E" } | |
| - var images = [{ id: '-gS54SWrHMg', width: 1000, height: 500 },{id: 'AU1rKyKPJco', width: 500, height: 1000, caption: 'Hello from Poland' },{ id: 'AXfDvKOawZQ' },{ id: 'gKlkZrsG_Pw', width: 1000, height: 500 },{ id: 'DVONaLgCRxo', width: 500, height: 1000 },{ id: 'o7txpYpxNLs', caption: 'Have a nice day' },{ id: 'ZsgUsl8GATg', width: 500, height: 1000, caption: 'This is a very very long description to show you it\'s possible to add something like this' },{ id: 'CkagyZJ88kE' },{ id: 'PpQ4-HOZ_8U', width: 1000, height: 500 },{ id: 'si7gjqJQj_8' },{ id: 'u0M0gyuexfE', width: 500, height: 1000 },{ id: 'aQcE3gDSSTY' },{ id: 'GkCafprWKRo', width: 500, height: 1000 },{ id: 'OFlzoTfpRdw' },{ id: 'YlFM0-LdHu8' },{ id: 'c_Tc9ZELeYw' }] | |
| mixin gallery-item(id, width=500, height=500, caption) | |
| if height > width | |
| - var klass = 'vertical' | |
| else if width > height | |
| - var klass = 'horizontal' | |
| figure.gallery-item(itemprop='associatedMedia', itemscope='', itemtype='http://schema.org/ImageObject', class=klass) | |
| a(href=`https://source.unsplash.com/${id}/${width*2}x${height*2}`, itemprop='contentUrl', data-size=`${width*2}x${height*2}`) | |
| img.lazyload.lazypreload.fadein(src=placeholder(width,height) data-src=`https://source.unsplash.com/${id}/${width}x${height}`, itemprop='thumbnail', alt='Image description') | |
| figcaption.gallery-caption(itemprop='caption description')= caption || 'Caption' | |
| .gallery(itemscope='', itemtype='http://schema.org/ImageGallery') | |
| each image in images | |
| +gallery-item(image.id,image.width,image.height, image.caption) | |
| // Root element of PhotoSwipe. Must have class pswp. | |
| .pswp(tabindex='-1', role='dialog', aria-hidden='true') | |
| // | |
| Background of PhotoSwipe. | |
| It's a separate element as animating opacity is faster than rgba(). | |
| .pswp__bg | |
| // Slides wrapper with overflow:hidden. | |
| .pswp__scroll-wrap | |
| // | |
| Container that holds slides. | |
| PhotoSwipe keeps only 3 of them in the DOM to save memory. | |
| Don't modify these 3 pswp__item elements, data is added later on. | |
| .pswp__container | |
| .pswp__item | |
| .pswp__item | |
| .pswp__item | |
| // Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. | |
| .pswp__ui.pswp__ui--hidden | |
| .pswp__top-bar | |
| // Controls are self-explanatory. Order can be changed. | |
| .pswp__counter | |
| button.pswp__button.pswp__button--close(title='Close (Esc)') | |
| button.pswp__button.pswp__button--share(title='Share') | |
| button.pswp__button.pswp__button--fs(title='Toggle fullscreen') | |
| button.pswp__button.pswp__button--zoom(title='Zoom in/out') | |
| // Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR | |
| // element will get class pswp__preloader--active when preloader is running | |
| .pswp__preloader | |
| .pswp__preloader__icn | |
| .pswp__preloader__cut | |
| .pswp__preloader__donut | |
| .pswp__share-modal.pswp__share-modal--hidden.pswp__single-tap | |
| .pswp__share-tooltip | |
| button.pswp__button.pswp__button--arrow--left(title='Previous (arrow left)') | |
| button.pswp__button.pswp__button--arrow--right(title='Next (arrow right)') | |
| .pswp__caption | |
| .pswp__caption__center |
Responsive gallery with:
A Pen by Michal Niewitala 🍋 on CodePen.
| initPhotoSwipeFromDOM = (gallerySelector) -> | |
| # parse slide data (url, title, size ...) from DOM elements | |
| # (children of gallerySelector) | |
| parseThumbnailElements = (el) -> | |
| thumbElements = el.childNodes | |
| numNodes = thumbElements.length | |
| items = [] | |
| figureEl = undefined | |
| linkEl = undefined | |
| size = undefined | |
| item = undefined | |
| i = 0 | |
| while i < numNodes | |
| figureEl = thumbElements[i] | |
| # <figure> element | |
| # include only element nodes | |
| if figureEl.nodeType != 1 | |
| i++ | |
| continue | |
| linkEl = figureEl.children[0] | |
| # <a> element | |
| size = linkEl.getAttribute('data-size').split('x') | |
| # create slide object | |
| item = | |
| src: linkEl.getAttribute('href') | |
| w: parseInt(size[0], 10) | |
| h: parseInt(size[1], 10) | |
| if figureEl.children.length > 1 | |
| # <figcaption> content | |
| item.title = figureEl.children[1].innerHTML | |
| if linkEl.children.length > 0 | |
| # <img> thumbnail element, retrieving thumbnail url | |
| item.msrc = linkEl.children[0].getAttribute('src') | |
| item.el = figureEl | |
| # save link to element for getThumbBoundsFn | |
| items.push item | |
| i++ | |
| items | |
| # find nearest parent element | |
| closest = (el, fn) -> | |
| el and (if fn(el) then el else closest(el.parentNode, fn)) | |
| # triggers when user clicks on thumbnail | |
| onThumbnailsClick = (e) -> | |
| e = e or window.event | |
| if e.preventDefault then e.preventDefault() else (e.returnValue = false) | |
| eTarget = e.target or e.srcElement | |
| # find root element of slide | |
| clickedListItem = closest(eTarget, (el) -> | |
| el.tagName and el.tagName.toUpperCase() == 'FIGURE' | |
| ) | |
| if !clickedListItem | |
| return | |
| # find index of clicked item by looping through all child nodes | |
| # alternatively, you may define index via data- attribute | |
| clickedGallery = clickedListItem.parentNode | |
| childNodes = clickedListItem.parentNode.childNodes | |
| numChildNodes = childNodes.length | |
| nodeIndex = 0 | |
| index = undefined | |
| i = 0 | |
| while i < numChildNodes | |
| if childNodes[i].nodeType != 1 | |
| i++ | |
| continue | |
| if childNodes[i] == clickedListItem | |
| index = nodeIndex | |
| break | |
| nodeIndex++ | |
| i++ | |
| if index >= 0 | |
| # open PhotoSwipe if valid index found | |
| openPhotoSwipe index, clickedGallery | |
| false | |
| # parse picture index and gallery index from URL (#&pid=1&gid=2) | |
| photoswipeParseHash = -> | |
| hash = window.location.hash.substring(1) | |
| params = {} | |
| if hash.length < 5 | |
| return params | |
| vars = hash.split('&') | |
| i = 0 | |
| while i < vars.length | |
| if !vars[i] | |
| i++ | |
| continue | |
| pair = vars[i].split('=') | |
| if pair.length < 2 | |
| i++ | |
| continue | |
| params[pair[0]] = pair[1] | |
| i++ | |
| if params.gid | |
| params.gid = parseInt(params.gid, 10) | |
| params | |
| openPhotoSwipe = (index, galleryElement, disableAnimation, fromURL) -> | |
| pswpElement = document.querySelectorAll('.pswp')[0] | |
| gallery = undefined | |
| options = undefined | |
| items = undefined | |
| items = parseThumbnailElements(galleryElement) | |
| # define options (if needed) | |
| options = | |
| galleryUID: galleryElement.getAttribute('data-pswp-uid') | |
| getThumbBoundsFn: (index) -> | |
| # See Options -> getThumbBoundsFn section of documentation for more info | |
| thumbnail = items[index].el.getElementsByTagName('img')[0] | |
| pageYScroll = window.pageYOffset or document.documentElement.scrollTop | |
| rect = thumbnail.getBoundingClientRect() | |
| { | |
| x: rect.left | |
| y: rect.top + pageYScroll | |
| w: rect.width | |
| } | |
| # PhotoSwipe opened from URL | |
| if fromURL | |
| if options.galleryPIDs | |
| # parse real index when custom PIDs are used | |
| # http://photoswipe.com/documentation/faq.html#custom-pid-in-url | |
| j = 0 | |
| while j < items.length | |
| if items[j].pid == index | |
| options.index = j | |
| break | |
| j++ | |
| else | |
| # in URL indexes start from 1 | |
| options.index = parseInt(index, 10) - 1 | |
| else | |
| options.index = parseInt(index, 10) | |
| # exit if index not found | |
| if isNaN(options.index) | |
| return | |
| if disableAnimation | |
| options.showAnimationDuration = 0 | |
| # Pass data to PhotoSwipe and initialize it | |
| gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options) | |
| gallery.init() | |
| return | |
| # loop through all gallery elements and bind events | |
| galleryElements = document.querySelectorAll(gallerySelector) | |
| i = 0 | |
| l = galleryElements.length | |
| while i < l | |
| galleryElements[i].setAttribute 'data-pswp-uid', i + 1 | |
| galleryElements[i].onclick = onThumbnailsClick | |
| i++ | |
| # Parse URL and open gallery if it contains #&pid=3&gid=1 | |
| hashData = photoswipeParseHash() | |
| if hashData.pid and hashData.gid | |
| openPhotoSwipe hashData.pid, galleryElements[hashData.gid - 1], true, true | |
| return | |
| # execute above function | |
| initPhotoSwipeFromDOM '.gallery' | |
| # --- | |
| # generated by js2coffee 2.2.0 |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/4.0.2/lazysizes.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.2/photoswipe.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.2/photoswipe-ui-default.js"></script> |
| %gallery-caption | |
| +absolute(bottom 4rem left 50%) | |
| transform: translate(-50%,0%) | |
| font-size: 12px | |
| +breakpoint($mobile) | |
| font-size: 14px | |
| color: rgba(white,0) | |
| padding: 1.25em 1.5em | |
| transition: all .2s ease | |
| font-weight: 600 | |
| // min-width: 10rem | |
| // width: 50% | |
| // max-width: calc(100% - 6rem) | |
| line-height: 1.25 | |
| text-align: center | |
| box-sizing: border-box | |
| pointer-events: none | |
| &:before, &:after | |
| content: '' | |
| +absolute(top right left bottom) | |
| background: rgba(black,1) | |
| width: 100% | |
| height: 100% | |
| transition: all .3s ease 0s | |
| z-index: -1 | |
| &:before | |
| top: auto | |
| height: 3px | |
| transform: scale(0,1) | |
| transform-origin: bottom left | |
| transition-delay: .6s | |
| &:after | |
| transform: scale(1,0) | |
| transform-origin: bottom | |
| transition-delay: .3s | |
| &.visible | |
| color: rgba(white,1) | |
| text-shadow: 0 0 1px rgba(black,.2) | |
| transition: all .3s ease .3s | |
| &:before | |
| transform: scale(1,1) | |
| transition-delay: 0s | |
| &:after | |
| transform: scale(1,1) | |
| &:empty | |
| display: none | |
| $corner: 1rem | |
| %image-border | |
| +relative | |
| &:before, &:after | |
| content: '' | |
| +absolute(top right left bottom) | |
| border: 0 solid rgba(black,.1) | |
| transition: all .2s | |
| will-change: border | |
| z-index: 10 | |
| &.active | |
| &:before | |
| border-width: .5rem | |
| &:after | |
| border-width: 2px | |
| &:after | |
| margin: $corner | |
| border: 2px solid rgba(white,.5) | |
| clip-path: polygon(0 calc(100% - #{$corner}), 0 100%, $corner 100%, $corner 0, 0 0, 0 $corner, 100% $corner, 100% 0, calc(100% - #{$corner}) 0, calc(100% - #{$corner}) 100%, 100% 100%, 100% calc(100% - #{$corner})) | |
| &:hover | |
| &:after | |
| transform: scale(.9) | |
| border-color: rgba(white,1) | |
| %caption-outside | |
| background-color: black | |
| color: white | |
| padding: .75em 1em | |
| display: inline-block | |
| text-align: left | |
| %gallery-grid | |
| +sans-serif-font | |
| width: 100% | |
| display: grid | |
| grid-template-rows: flow | |
| grid-auto-flow: dense | |
| +breakpoint(max-width $mobile - 1px) | |
| grid-template-columns: repeat(2, 1fr) | |
| +breakpoint($mobile $desktop - 1px) | |
| grid-template-columns: repeat(3, 1fr) | |
| +breakpoint($desktop $laptop - px) | |
| grid-template-columns: repeat(4, 1fr) | |
| +breakpoint($laptop $screen - 1px) | |
| grid-template-columns: repeat(5, 1fr) | |
| +breakpoint($screen) | |
| grid-template-columns: repeat(6, 1fr) | |
| %gallery-item | |
| +relative | |
| background-color: rgba(black,.5) | |
| overflow: hidden | |
| img, a | |
| display: block | |
| &.vertical | |
| grid-row: span 2 | |
| &.horizontal | |
| grid-column: span 2 | |
| .gallery | |
| @extend %gallery-grid | |
| .gallery-item | |
| @extend %gallery-item | |
| a | |
| display: block | |
| @extend %image-border | |
| .lazy-images &.image-lazyloaded, html:not(.lazy-images) & | |
| @extend %image-border.active | |
| .gallery-caption | |
| html:not(.touchevents) & | |
| @extend %gallery-caption | |
| .gallery-item:hover & | |
| @extend %gallery-caption.visible | |
| [class*=list] &, .gallery-size-thumbnail & | |
| display: none |
| <link href="https://codepen.io/mican/pen/xYpoWX" rel="stylesheet" /> | |
| <link href="https://codepen.io/mican/pen/yoOYLZ" rel="stylesheet" /> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.2/photoswipe.css" rel="stylesheet" /> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.2/default-skin/default-skin.min.css" rel="stylesheet" /> |