HomeCSS Tailwind
Chapter 13
13 β Responsive Images & Media
Image Formats
| Format | Use Case | Browser Support |
|---|---|---|
| WebP | Modern web, 25-35% smaller than JPEG | Chrome, Firefox, Safari, Edge |
| AVIF | Best compression, 50% smaller than JPEG | Chrome, Firefox, Opera |
| JPEG | Photos, gradients | All |
| PNG | Transparency, graphics | All |
| SVG | Icons, logos, illustrations | All |
| GIF | Simple animations (use video instead) | All |
<picture> Element
<picture>
<!-- AVIF for modern browsers -->
<source
type="image/avif"
srcset="image-300.avif 300w,
image-600.avif 600w,
image-1200.avif 1200w">
<!-- WebP fallback -->
<source
type="image/webp"
srcset="image-300.webp 300w,
image-600.webp 600w,
image-1200.webp 1200w">
<!-- JPEG fallback -->
<source
type="image/jpeg"
srcset="image-300.jpg 300w,
image-600.jpg 600w,
image-1200.jpg 1200w">
<!-- Fallback img -->
<img
src="image-600.jpg"
alt="Description"
loading="lazy"
width="600"
height="400">
</picture>srcset and sizes
Resolution Switching
<!-- Serve different resolutions -->
<img
src="image-800.jpg"
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="Description">How it works:
- Viewport β€ 600px: Use 100% of viewport width β picks 400w or 800w
- Viewport β€ 1200px: Use 50% of viewport width β picks 800w
- Viewport > 1200px: Use 33% of viewport width β picks 1200w
Art Direction
<picture>
<!-- Wide image for desktop -->
<source
media="(min-width: 1024px)"
srcset="hero-wide.jpg">
<!-- Tall image for mobile -->
<source
media="(max-width: 767px)"
srcset="hero-tall.jpg">
<!-- Default -->
<img src="hero-default.jpg" alt="Hero">
</picture>Lazy Loading
Native Lazy Loading
<!-- Lazy load images -->
<img src="image.jpg" loading="lazy" alt="...">
<!-- Eager load (above the fold) -->
<img src="hero.jpg" loading="eager" alt="...">
<!-- Lazy load iframes -->
<iframe src="video.html" loading="lazy"></iframe>JavaScript Lazy Loading
// Intersection Observer API
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));<!-- Markup -->
<img
data-src="image.jpg"
src="placeholder.jpg"
alt="..."
class="lazyload">Image Optimization
Compression Tools
# Squoosh (Web-based)
https://squoosh.app
# ImageMagick
convert input.jpg -quality 80 output.jpg
# Sharp (Node.js)
npm install sharp// Sharp example
const sharp = require('sharp');
sharp('input.jpg')
.resize(800, 600)
.webp({ quality: 80 })
.toFile('output.webp');CDN Optimization
<!-- Cloudinary -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,h_600,c_fill,q_auto/photo.jpg">
<!-- Imgix -->
<img src="https://example.imgix.net/photo.jpg?w=800&h=600&fit=crop&auto=format">
<!-- Cloudflare Images -->
<img src="https://imagedelivery.net/abc123/photo/public">Video Optimization
<!-- Preload metadata only -->
<video
src="video.mp4"
preload="metadata"
poster="poster.jpg"
controls>
</video>
<!-- Multiple formats -->
<video controls>
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
Your browser doesn't support HTML5 video.
</video>
<!-- Lazy load video -->
<video
src="video.mp4"
preload="none"
poster="poster.jpg"
controls></video>Autoplay (Muted)
<video
autoplay
muted
loop
playsinline
poster="poster.jpg">
<source src="background.mp4" type="video/mp4">
</video>Background Images
/* Responsive background */
.hero {
background-image: url('small.jpg');
background-size: cover;
background-position: center;
}
@media (min-width: 768px) {
.hero {
background-image: url('large.jpg');
}
}<!-- Inline styles for dynamic images -->
<div
class="hero"
style="background-image: url('image.jpg')">
</div>Aspect Ratio
<!-- Modern CSS -->
<div class="aspect-video">
<img src="video-thumbnail.jpg" alt="...">
</div>
<div class="aspect-square">
<img src="image.jpg" alt="...">
</div>
<!-- Custom aspect ratio -->
<div class="aspect-[4/3]">
Content
</div>/* Manual aspect ratio */
.aspect-ratio-box {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
}
.aspect-ratio-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}Blur Placeholders
<div class="relative">
<!-- Blurred placeholder -->
<img
src="placeholder-blur.jpg"
class="absolute inset-0 w-full h-full object-cover blur-sm">
<!-- Actual image -->
<img
src="actual-image.jpg"
loading="lazy"
class="relative w-full h-full object-cover">
</div>/* LQIP (Low Quality Image Placeholder) */
.image-container {
background: url('lqip.jpg') no-repeat center;
background-size: cover;
}
.image-container img {
opacity: 0;
transition: opacity 0.3s;
}
.image-container img.loaded {
opacity: 1;
}Responsive <figure> and <figcaption>
<figure>
<picture>
<source media="(min-width: 1024px)" srcset="image-large.jpg">
<source media="(min-width: 768px)" srcset="image-medium.jpg">
<img src="image-small.jpg" alt="Description" loading="lazy">
</picture>
<figcaption>
<strong>Figure 1:</strong> Description of the image
</figcaption>
</figure>Performance Tips
β
Use modern formats (WebP, AVIF)
β
Compress images (aim for <100KB per image)
β
Specify width and height (prevents layout shift)
β
Lazy load below-the-fold images
β
Use CDN for automatic optimization
β
Serve responsive images with srcset
β
Preload critical images
β
Use blur placeholders for better UX
β
Optimize video (compress, use poster)
β
Set proper caching headersKey Takeaways
- Use
<picture>for art direction,srcsetfor resolution switching - WebP/AVIF = smaller file sizes, JPEG/PNG = fallback
- Lazy loading =
loading="lazy"or Intersection Observer - CDN optimization = automatic resizing, compression, format conversion
- Aspect ratio = prevent layout shift with
aspect-ratioor padding hack - Blur placeholders = better perceived performance
- Always specify
widthandheightto prevent CLS - Preload critical above-the-fold images