Sam Dutton
Sam Dutton

Reputation: 15269

Responsive images: set sizes attribute from JavaScript

The srcset attribute can be set from JavaScript to implement responsive lazy loaded images.

However, setting the sizes attribute doesn't seem to work. Coding sizes in HTML works as expected, but when I try to set the sizes attribute from JavaScript (as in the code below) it has no effect: the image chosen matches the viewport width and doesn't follow the sizes hints.

Is this by design, or has this just not been implemented?

Sample code below.

const BASE_URL = 'https://res.cloudinary.com/foobar/f_auto,dpr_auto,q_auto:eco/';

const SIZES = {
  0: 'calc(100vw - 60px)',
  420: 'calc((100vw - 90px) / 2)',
  750: 'calc((100vw - 120px) / 3)',
  1200: 'calc((100vw - 150px) / 4)'
}

const WIDTHS = [500, 1000, 1500];

const options = {
  rootMargin: '0px 0px 100px 0px',
};

function callback(entries) {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      const lazyImage = entry.target;
      const id = lazyImage.dataset.id;
      lazyImage.sizes = getSizes();
      lazyImage.srcset = getSrcset(id);
      io.unobserve(lazyImage);
    }
  }
}

function getSrcset(id) {
  const srcset = [];
  for (const width of WIDTHS) {
    srcset.push(`${BASE_URL}w_${width}/${id}.jpg ${width}w`);
  }
  return srcset.join(',');
}

function getSizes() {
  const sizes = [];
  for (const size in SIZES) {
    sizes.push(`(min-width: ${size}px) ${SIZES[size]}`);
  }
  return sizes.join(',');
}

const images = document.querySelectorAll('img.lazy');

let io;
if (window.IntersectionObserver) {
  io = new IntersectionObserver(callback, options);
}

for (const image of images) {
  if (window.IntersectionObserver) {
    io.observe(image);
  } else {
    console.log('Intersection Observer not supported');
    image.src = BASE_URL + image.getAttribute('data-id' + '.jpg');
  }
}

Upvotes: 1

Views: 1792

Answers (2)

Sam Dutton
Sam Dutton

Reputation: 15269

As per @amed0zmey's comment, I fixed this by getting the sizes code right.

Working example below.

const BASE_URL = 'https://res.cloudinary.com/foo/f_auto,dpr_auto,q_auto:eco/';

const BREAKPOINTS = [0, 420, 750, 1200];
const DISPLAY_WIDTHS = ['calc(100vw - 60px)', 'calc((100vw - 90px) / 2)',
  'calc((100vw - 120px) / 3)', 'calc((100vw - 150px) / 4)'];

const IMAGE_WIDTHS = [500, 1000, 1500];

const options = {
  rootMargin: '0px 0px 100px 0px',
};

function callback(entries) {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      const image = entry.target;
      const id = image.dataset.id;
      image.srcset = getSrcset(id);
      io.unobserve(image);
    }
  }
}

function getSrcset(id) {
  const srcset = [];
  for (const width of IMAGE_WIDTHS) {
    srcset.push(`${BASE_URL}w_${width}/${id}.jpg ${width}w`);
  }
  return srcset.join(',');
}

function getSizes() {
  const sizes = [];
  for (let i = 0; i !== BREAKPOINTS.length; ++i) {
    let size = `(min-width: ${BREAKPOINTS[i]}px) `;
    const nextBreakpoint = BREAKPOINTS[i + 1];
    if (nextBreakpoint) {
      size += `and (max-width: ${nextBreakpoint}px) `;
    }
    size += DISPLAY_WIDTHS[i];
    sizes.push(size);
  }
  return sizes.join(',');
}

const images = document.querySelectorAll('img.lazy');

let io;
if (window.IntersectionObserver) {
  io = new IntersectionObserver(callback, options);
}

for (const image of images) {
  image.sizes = getSizes();
  if (window.IntersectionObserver) {
    io.observe(image); 
  } else {
    console.log('Intersection Observer not supported');
    const id = image.getAttribute('data-id');
    image.srcset = getSrcset(id);
    image.src = BASE_URL + 'id' + '.jpg';
  }
}

Upvotes: 0

akmed0zmey
akmed0zmey

Reputation: 573

I ran your sample and it works fine and as expected for me.

Something is wrong with how sizes are coded, probably.
I think it's not a good idea to put min-width: 0px media condition since it will always be selected and all other conditions will be ignored.

From here https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images

The browser ignores everything after the first matching condition, so be careful how you order the media conditions.

Upvotes: 1

Related Questions