In today’s digital age, having a website that adapts to different devices is essential. With so many different screen sizes and resolutions, responsive design has become the norm. CSS media-queries are often used to create responsive layouts, but they have some limitations when it comes to serving optimised media. This is where JavaScript comes in handy.
JavaScript can be used to create custom media-queries that can save bandwidth and speed up the load time of your website. While CSS media-queries and the HTML srcset attribute can be used to tell the browser which assets to download and when, JavaScript can provide more advanced functionality.
With a fairly simple JavaScript code, you can create a custom, powerful media-query that checks the size of the image container rather than just the viewport. This can help you serve the appropriate image size for different container sizes.
In addition, JavaScript can check pixel-density as well as the viewport/container size at the same time, unlike the HTML srcset attribute. This means that you can serve the appropriate image size for different pixel densities and container sizes, which can result in faster load times and better user experience.
One of the advantages of using JavaScript for breakpoints is that it allows for seamless transitions when swapping assets. There is no need for a reload on resize-down, which can result in a better user experience. However, creating custom media-queries with JavaScript can be more complicated and sometimes tricky to debug. It also requires additional CSS and some back-end code.
Responsive images in custom WordPress theme
In this chapter, we will delve deeper into the world of JavaScript-powered breakpoint systems. We will explore how to use breakpoint systems to manage responsive images, and discuss best practices for implementation.
To achieve the best results with JavaScript-powered breakpoints, breakpoint values should be defined in a JSON or database, so both back-end and the JavaScript code can use them. In addition, all elements of the imgset should be of the same proportions to ensure that the appropriate image size is served for each container size.
The below example shows the HTML code for a single imgset – both <img> tag and also background-image attribute.
<!-- IMG tag -->
<figure class="imgset">
<img class="imgset-el ar-hero-img preview hide" src="example-122x69.jpg" height="69" width="122" alt="">
<img class="imgset-el ar-hero-img jsbp def def-x2" data-img-src="example-900x506.jpg" height="506" width="900" alt="">
<img src="example-1800x1013.jpg" class="imgset-el ar-hero-img jsbp mini-x2 small loaded bp-current" data-img-src="example-1800x1013.jpg" height="1013" width="1800" alt="">
<img class="imgset-el ar-hero-img jsbp small-x2 xlarge" data-img-src="example-2880x1620.jpg" height="1620" width="2880" alt="">
</figure>
<!-- background-image -->
<figure class="imgset">
<div class="preview hide" style="background-image:url(example-122x122.jpg)"></div>
<div class="jsbp def def-x2" data-img-src="example-900x900.jpg"></div>
<div class="jsbp mini-x2 small loaded bp-current" data-img-src="example-1800x1800.jpg" style="background-image: url('example-1800x1800.jpg');"></div>
<div class="jsbp small-x2 xlarge" data-img-src="example-2880x2880.jpg"></div>
<div class="jsbp xlarge-x2 bp4k" data-img-src="example-4000x4000.jpg"></div>
</figure>
The HTML classes of each of the imgset elements represent the breakpoint names. The JS script traverses all elements from the smallest to the largest and tell the browser to download the first element that meets the current display and/or container setup.
As in the shown code snippet, only 4 images, 5 if you count the low-resolution preview, are really needed to serve all common screen sizes. No need for a typical approach with doubling or tripling the asses in order to satisfy all pixel-densities.
To achieve best results all elements of the imgset should be of the same proportions.
##/
#
# Responsive imageset / sourcest, js powered
#
# main difference vs CSS media queries
# * ignores window resize down when large images have been already downloaded
# * can use any html container as a reference rather than just the viewport, yay!
#
# Please note!
# The image loader relies on browser cache so might produce strange effect when testing with cache disabled.
#
#
# *** *** *** *** *** ***
# SETUP & DEPENDENCIES
#
breakpoints= @import '../../settings/breakpoints.json'
px_density_suffix= '' # default fallback 1x
# *** *** ***
# SUBROUTINES
#
##
# pix density detection in different os/browsers
#
# ___checking only up to x2 at the moment, can be increased if needed
#
#
isHighDensity = ->
window.matchMedia and (window.matchMedia('only screen and (min-resolution: 124dpi), only screen and (min-resolution: 1.3dppx), only screen and (min-resolution: 48.8dpcm)').matches or window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3)').matches) or window.devicePixelRatio and window.devicePixelRatio > 1.3
isRetina = ->
(window.matchMedia and (window.matchMedia('only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)').matches or window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)').matches) or window.devicePixelRatio and window.devicePixelRatio >= 2) and /(iPad|iPhone|iPod)/g.test(navigator.userAgent)
##
# # # #
#
getCurrentPxDensitySuffix= () ->
if isHighDensity() or isRetina()
return '-x2'
else
return ''
##/
#
# 'Is the element's size good for current breakpoint or what...?'
#
# Check if the imgset container is in within the scope of breakpoints (can check for multiple bps simultaneously)
# args (bp names) can be passed as an array or a splat
# default is lower limit (min) unless 'max' keyword is added such as e. g. max-small
# the pixel density and orientation can also be added such as e. g. max-xlarge-vert-x2 (the order of components doesn't matter)
# container element must be passed as first argument, otherwise the $(window) is used
#
el_is_in_bp= (args...) ->
return if not args[0]
# args can be also passed as array
args= args[0] if args[0].constructor == Array
# can pass the container element, otherwise using window
if args[0] instanceof jQuery
$container= args[0]
args.shift()
else
$container= $(window)
retval= true
$.each args, (ix, arg) ->
# split argument to components or just convert to a single elem array
if arg.indexOf('-') > -1 then arg= arg.split '-' else arg= [arg]
# find the breakpoint name in arg's components (arrays intersect)
_bp= Object.keys(breakpoints).filter (n) ->
arg.indexOf(n) > -1
# no bp means the default image (always true)
if 0==_bp.length or 'default'==_bp then return true
# return false after first condition met (negative testing)
#
# pix density
# max 1x -- (not really used much :)
if arg.indexOf('x1') > -1 or arg.indexOf('1x') > -1
if isRetina() or isHighDensity()
retval= false
return false # break
# min x2
if arg.indexOf('x2') > -1 or arg.indexOf('2x') > -1
if not isRetina() and not isHighDensity()
retval= false
return false # break
# size
#
# upper limit
if arg.indexOf('max') > -1
if arg.indexOf('vert') > -1 or arg.indexOf('vertical') > -1
if $container.height() >= breakpoints[_bp]
retval= false
return false # break
else
if $container.width() >= breakpoints[_bp]
retval= false
return false # break
# lower limit
else
if arg.indexOf('vert') > -1 or arg.indexOf('vertical') > -1
if $container.height() < breakpoints[_bp]
retval= false
return false # break
else
if $container.width() < breakpoints[_bp]
retval= false
return false # break
# returns true if none of condition are met
return retval
##/
#
#
# Loads images to browser cache and once downloaded updates background-image src attribute
#
# # Note!
# The image loader relies on browser cache so might produce strange effect when testing with cache disabled.
# ___won't work correctly if the browser's cache is turned off, will load the images twice :-(
#
# @param $elms_queue -- array, a list of html elements to process
# the src needs to be specified in the html element's attribute 'data-bgd-src'
#
#
loadImgsetsImgs= ($elms_queue) ->
return if 0==$elms_queue.length
# recoursive function to
# load one img at a time for faster response
_process_q= ($elms_queue) ->
_$el= $elms_queue.shift()
return if _$el.hasClass('.loading')
if _$el.hasClass('.loaded')
_$el.siblings('.bp-current').removeClass 'bp-current'
_$el.addClass 'bp-current'
return
_loader_img= new Image
_loader_img.src= _$el.attr 'data-img-src'
if false == _loader_img.complete # cannot load image
# debug missing images
# console.log 'Error loading img '+_$el.parent().attr('class')+', size:'+_$el.attr('class')
else
_$el.addClass 'loading'
$(_loader_img).on 'load', ->
##
# set either src attribute or the background-image depending on setup
#
if _$el.prop('tagName') == 'IMG'
_$el.attr 'src', @src
else
_$el.css 'background-image', 'url('+@src+')'
_$el.removeClass 'loading'
_$el.addClass 'loaded'
_$el.siblings('.bp-current').addClass 'hiding'
_$el.siblings('.bp-current').removeClass 'bp-current'
_$el.addClass 'bp-current'
_$el.siblings('.preview').addClass('hiding') if not _$el.siblings('.preview').hasClass 'hide'
setTimeout () ->
_$el.siblings('.hiding').removeClass 'hiding'
, 1000
_$el.siblings('.preview').addClass 'hide'
return # end on-load
# prevent memory leak
delete $(_loader_img)
#_loader_img.remove() # that breaks IE
# recurse if items in queue
_process_q $elms_queue if $elms_queue.length>0
return # end process_queue
_process_q $elms_queue
return # end loadImgsetsImgs
##/
#
#
# Load and show the image of the imgset according to current container or viewport size/px density
#
#
#
# By default it doesn't load smaller image if the viewport size is decreased
# This can be overridden by adding data attribute to the imgset html selector:
# ====| data-bp-always-reload=true |====
#
#
refreshImgsets= () ->
# create reversed lists of breakpoint keys for looping through
_bps_keys_rev = Object.keys(breakpoints).reverse()
_$els_to_load = []
$('.imgset').each ->
_$imgset = $(@)
# console.log _$imgset.attr('class')
# an option that enables loading smaller images on resize down (as in classic CSS media queries)
always_reload = false
always_reload = true if _$imgset.attr('data-bp-always-reload') and 1==_$imgset.attr('data-bp-always-reload')
_$load = undefined
_bp_loaded = undefined
_bp_needed = undefined
_load_is_a_match = false
_$match = undefined
# find breakpoint images already loaded (from largest to smallest)
$.each _bps_keys_rev, (ix, val) ->
$el= _$imgset.find('.jsbp.loaded.'+val+px_density_suffix)
if $el.length > 0
_$load = $el
_bp_loaded = val+px_density_suffix
return false # break 1
##
# use the first (largest) found one for now
if _$load
_$imgset.find('.bp-current').removeClass 'bp-current'
_$load.addClass 'bp-current'
##
# check if we need and can find a better one
# find first element in imgset that matches the current bp (again, from largst to smallest)
$.each _bps_keys_rev, (ix, val) ->
_$imgset.find('.jsbp').each ->
# check if loaded one matches and set a flag
if val+px_density_suffix==_bp_loaded
_load_is_a_match= true
# check if element's css classes contain bp of current iteration and if it's the current device's bp
if $(@).attr('class').split(' ').indexOf(val+px_density_suffix) > -1 and el_is_in_bp(_$imgset, val+px_density_suffix)
if always_reload or not _load_is_a_match
_bp_needed= val+px_density_suffix
_$match= $(@)
return false # break 1
return false if _$match # break 1
# get the first element (smallest bp possible) if different than the loaded one
if _bp_needed
_$els_to_load.push _$match
loadImgsetsImgs _$els_to_load
return
# *** *** ***
# RUNTIME
#
px_density_suffix= getCurrentPxDensitySuffix()
refreshImgsets()
# *** *** ***
# utils timeout events
imgsets_window_resize_tout = undefined
$(window).resize ->
if imgsets_window_resize_tout
window.clearTimeout imgsets_window_resize_tout
imgsets_window_resize_tout = window.setTimeout((->
# actual callback
px_density_suffix= getCurrentPxDensitySuffix()
refreshImgsets()
return
), 500)
return
Custom breakpoint definitions used by the JSBP scripts.
{
"def" : 0,
"micro" : 410,
"mini" : 481,
"xxsmall" : 600,
"xsmall" : 736,
"small" : 768,
"medium" : 961,
"large" : 1041,
"mlarge" : 1281,
"xlarge" : 1441,
"xxlarge" : 1881,
"bp4k" : 2880
}
In summary, while CSS media-queries and the HTML srcset attribute are useful for creating responsive layouts, JavaScript can be used to create custom media-queries that provide more advanced functionality. By using JavaScript to serve optimised media, you can save bandwidth and speed up the load time of your website, resulting in a better user experience.
Add a comment