5.4: Adding Media & Interactive Elements

Enhance your website with optimized images, videos, and interactive features to create an engaging user experience. Learn image optimization, responsive media, and how to add interactivity using forms and JavaScript.

1. Image Optimization Basics

Images are often the largest files on web pages. Optimize them for fast loading.

Image Format Selection

JPG (JPEG) - Best for photos:

✓ Photographs and complex images
✓ Millions of colors
✓ Good compression
✗ Lossy compression (quality loss)
✗ No transparency
✗ Not good for text/logos

File sizes: 50-500 KB (optimized photos)

PNG - Best for graphics with transparency:

✓ Logos, icons, illustrations
✓ Lossless compression
✓ Transparency support
✓ Sharp text and edges
✗ Larger file sizes
✗ Not ideal for photos

File sizes: 10-200 KB (icons/logos)

WebP - Modern format (best overall):

✓ Better compression than JPG/PNG
✓ Supports transparency
✓ Lossy and lossless options
✓ 25-35% smaller files
✗ Older browsers don't support it

File sizes: 30-70% smaller than JPG/PNG

SVG - Best for icons and simple graphics:

✓ Infinitely scalable (vector)
✓ Tiny file sizes
✓ CSS styleable
✓ Animatable
✗ Not good for photos
✗ Complex for detailed illustrations

File sizes: 1-20 KB (icons)

Image Optimization Tools

Online tools:

TinyPNG       - PNG/JPG compression (https://tinypng.com)
Squoosh       - Modern image converter (https://squoosh.app)
ImageOptim    - Mac app for optimization
JPEG-Optimizer - Batch JPG optimization

Command-line tools:

# Convert to WebP (requires cwebp)
cwebp input.jpg -q 80 -o output.webp

# Optimize JPG
jpegoptim --max=85 image.jpg

# Optimize PNG
optipng -o5 image.png

Portfolio images:

Hero background:     1920 × 1080 px (max 300 KB)
Project thumbnail:   800 × 600 px (max 150 KB)
Project detail:      1200 × 900 px (max 200 KB)
Profile photo:       400 × 400 px (max 50 KB)
Logo:                SVG or 200 × 100 px (max 20 KB)
Icons:               SVG or 32 × 32 px (max 5 KB)

2. Responsive Images

Serve different image sizes for different devices.

Using srcset for Resolution Switching

Basic responsive image:

<img
  src="project-800.jpg"
  srcset="
    project-400.jpg 400w,
    project-800.jpg 800w,
    project-1200.jpg 1200w
  "
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="E-commerce platform screenshot"
>

Explanation:

  • src - Fallback for old browsers
  • srcset - List of image sources with widths (400w = 400px wide)
  • sizes - Tells browser how wide image will be displayed
    • Mobile (≤768px): 100% viewport width
    • Desktop: 50% viewport width
  • Browser chooses best image automatically

Using picture Element for Art Direction

Different crops for different screens:

<picture>
  <!-- Mobile: Square crop -->
  <source
    media="(max-width: 768px)"
    srcset="hero-mobile-400.jpg 400w,
            hero-mobile-800.jpg 800w"
  >

  <!-- Tablet: 4:3 crop -->
  <source
    media="(max-width: 1024px)"
    srcset="hero-tablet-800.jpg 800w,
            hero-tablet-1200.jpg 1200w"
  >

  <!-- Desktop: Wide 16:9 crop -->
  <source
    srcset="hero-desktop-1200.jpg 1200w,
            hero-desktop-1920.jpg 1920w"
  >

  <!-- Fallback -->
  <img src="hero-desktop-1200.jpg" alt="Portfolio hero image">
</picture>

Modern Format with Fallback

Serve WebP to modern browsers, JPG to others:

<picture>
  <!-- Modern browsers get WebP -->
  <source
    type="image/webp"
    srcset="project-400.webp 400w,
            project-800.webp 800w,
            project-1200.webp 1200w"
  >

  <!-- Older browsers get JPG -->
  <source
    type="image/jpeg"
    srcset="project-400.jpg 400w,
            project-800.jpg 800w,
            project-1200.jpg 1200w"
  >

  <!-- Fallback -->
  <img src="project-800.jpg" alt="Project screenshot">
</picture>

3. Image Galleries with Grid/Flexbox

HTML structure:

<section class="gallery">
  <h2>Project Screenshots</h2>

  <div class="gallery-grid">
    <figure class="gallery-item">
      <img src="images/screenshot-1.jpg" alt="Homepage design">
      <figcaption>Homepage</figcaption>
    </figure>

    <figure class="gallery-item">
      <img src="images/screenshot-2.jpg" alt="Product page">
      <figcaption>Product Page</figcaption>
    </figure>

    <figure class="gallery-item">
      <img src="images/screenshot-3.jpg" alt="Checkout flow">
      <figcaption>Checkout</figcaption>
    </figure>

    <figure class="gallery-item">
      <img src="images/screenshot-4.jpg" alt="Admin dashboard">
      <figcaption>Admin Dashboard</figcaption>
    </figure>

    <figure class="gallery-item">
      <img src="images/screenshot-5.jpg" alt="Mobile responsive">
      <figcaption>Mobile View</figcaption>
    </figure>

    <figure class="gallery-item">
      <img src="images/screenshot-6.jpg" alt="User profile">
      <figcaption>User Profile</figcaption>
    </figure>
  </div>
</section>

CSS Grid layout:

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.5rem;
  padding: 2rem 0;
}

.gallery-item {
  margin: 0;
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s, box-shadow 0.3s;
}

.gallery-item:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}

.gallery-item img {
  width: 100%;
  height: 250px;
  object-fit: cover;
  display: block;
}

.gallery-item figcaption {
  padding: 1rem;
  background-color: white;
  text-align: center;
  font-size: 0.875rem;
  color: #64748b;
}

CSS for masonry layout:

.masonry-gallery {
  column-count: 3;
  column-gap: 1rem;
}

@media (max-width: 1024px) {
  .masonry-gallery {
    column-count: 2;
  }
}

@media (max-width: 640px) {
  .masonry-gallery {
    column-count: 1;
  }
}

.masonry-item {
  break-inside: avoid;
  margin-bottom: 1rem;
}

.masonry-item img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 8px;
}

4. Modal Image Viewer (Lightbox)

Create a full-screen image viewer for gallery images.

HTML Structure

<!-- Gallery item with click handler -->
<figure class="gallery-item" data-image="images/project-1-full.jpg">
  <img src="images/project-1-thumb.jpg" alt="Project screenshot">
  <figcaption>Click to enlarge</figcaption>
</figure>

<!-- Modal (initially hidden) -->
<div class="modal" id="image-modal">
  <button class="modal-close" aria-label="Close modal">&times;</button>
  <img src="" alt="" id="modal-image">
  <div class="modal-caption" id="modal-caption"></div>
</div>

CSS for Modal

/* Modal overlay */
.modal {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.9);
  z-index: 1000;
  justify-content: center;
  align-items: center;
  padding: 2rem;
}

.modal.active {
  display: flex;
}

/* Modal image */
#modal-image {
  max-width: 90%;
  max-height: 90%;
  object-fit: contain;
  animation: zoomIn 0.3s;
}

@keyframes zoomIn {
  from {
    transform: scale(0.8);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

/* Close button */
.modal-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: none;
  border: none;
  color: white;
  font-size: 3rem;
  cursor: pointer;
  line-height: 1;
  padding: 0.5rem;
  transition: transform 0.2s;
}

.modal-close:hover {
  transform: scale(1.1);
}

/* Caption */
.modal-caption {
  position: absolute;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  color: white;
  background-color: rgba(0, 0, 0, 0.7);
  padding: 1rem 2rem;
  border-radius: 4px;
  max-width: 80%;
  text-align: center;
}

JavaScript for Modal Functionality

// Get modal elements
const modal = document.getElementById('image-modal');
const modalImage = document.getElementById('modal-image');
const modalCaption = document.getElementById('modal-caption');
const closeBtn = document.querySelector('.modal-close');

// Get all gallery items
const galleryItems = document.querySelectorAll('.gallery-item');

// Add click event to each gallery item
galleryItems.forEach(item => {
  item.addEventListener('click', () => {
    const imageSrc = item.dataset.image;
    const imageAlt = item.querySelector('img').alt;

    modalImage.src = imageSrc;
    modalImage.alt = imageAlt;
    modalCaption.textContent = imageAlt;

    modal.classList.add('active');
    document.body.style.overflow = 'hidden'; // Prevent scrolling
  });
});

// Close modal
function closeModal() {
  modal.classList.remove('active');
  document.body.style.overflow = ''; // Restore scrolling
}

closeBtn.addEventListener('click', closeModal);

// Close on outside click
modal.addEventListener('click', (e) => {
  if (e.target === modal) {
    closeModal();
  }
});

// Close on Escape key
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && modal.classList.contains('active')) {
    closeModal();
  }
});

5. Embedding Video

HTML5 Video Element

Basic video with controls:

<video
  controls
  width="800"
  poster="video-thumbnail.jpg"
>
  <source src="videos/demo.mp4" type="video/mp4">
  <source src="videos/demo.webm" type="video/webm">
  <p>
    Your browser doesn't support HTML5 video.
    <a href="videos/demo.mp4">Download the video</a> instead.
  </p>
</video>

Video with more options:

<video
  controls
  autoplay
  muted
  loop
  preload="metadata"
  poster="poster.jpg"
  width="100%"
  style="max-width: 800px;"
>
  <source src="demo.mp4" type="video/mp4">
  <source src="demo.webm" type="video/webm">

  <!-- Subtitles/captions -->
  <track
    kind="subtitles"
    src="subtitles-en.vtt"
    srclang="en"
    label="English"
  >

  <!-- Fallback -->
  <p>Video not supported. <a href="demo.mp4">Download video</a></p>
</video>

Attributes explained:

  • controls - Show play/pause/volume controls
  • autoplay - Start playing automatically (requires muted)
  • muted - No sound (required for autoplay)
  • loop - Repeat video
  • preload - "none", "metadata", or "auto"
  • poster - Thumbnail image before play

Embedding YouTube Videos

Responsive YouTube embed:

<!-- Container for responsive aspect ratio -->
<div class="video-wrapper">
  <iframe
    src="https://www.youtube.com/embed/VIDEO_ID"
    title="Project demo video"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen
  ></iframe>
</div>

CSS for responsive 16:9 video:

.video-wrapper {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 aspect ratio */
  height: 0;
  overflow: hidden;
  max-width: 100%;
}

.video-wrapper iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

Background Video (Hero Sections)

HTML for background video:

<section class="hero-video">
  <video autoplay muted loop playsinline class="hero-video-bg">
    <source src="videos/background.mp4" type="video/mp4">
    <source src="videos/background.webm" type="video/webm">
  </video>

  <div class="hero-content">
    <h1>Welcome to My Portfolio</h1>
    <p>Creating exceptional digital experiences</p>
  </div>
</section>

CSS for background video:

.hero-video {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.hero-video-bg {
  position: absolute;
  top: 50%;
  left: 50%;
  min-width: 100%;
  min-height: 100%;
  width: auto;
  height: auto;
  transform: translate(-50%, -50%);
  z-index: -1;
  object-fit: cover;
}

.hero-content {
  position: relative;
  z-index: 1;
  text-align: center;
  color: white;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
}

6. Audio Elements

HTML5 Audio Player

Basic audio player:

<audio controls>
  <source src="audio/podcast.mp3" type="audio/mpeg">
  <source src="audio/podcast.ogg" type="audio/ogg">
  <p>Your browser doesn't support audio playback.</p>
</audio>

Custom styled audio player (simple):

<div class="audio-player">
  <audio id="audio" src="audio/track.mp3"></audio>

  <button id="play-pause" class="btn-play">
    <span class="play-icon">▶</span>
    <span class="pause-icon" hidden>⏸</span>
  </button>

  <div class="progress-bar">
    <div class="progress" id="progress"></div>
  </div>

  <span id="time">0:00 / 0:00</span>
</div>

7. Icon Systems

SVG Icons (Inline)

Inline SVG for styling flexibility:

<!-- GitHub icon -->
<a href="https://github.com/username" class="social-link">
  <svg class="icon" width="24" height="24" viewBox="0 0 24 24">
    <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
  </svg>
  GitHub
</a>

CSS for SVG icons:

.icon {
  width: 24px;
  height: 24px;
  fill: currentColor; /* Inherits text color */
  vertical-align: middle;
  transition: fill 0.3s;
}

.social-link:hover .icon {
  fill: #6366f1;
}

Icon Fonts (Font Awesome)

Using Font Awesome CDN:

<!-- In <head> -->
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
>

<!-- In HTML -->
<a href="https://github.com/username">
  <i class="fab fa-github"></i> GitHub
</a>

<a href="https://linkedin.com/in/username">
  <i class="fab fa-linkedin"></i> LinkedIn
</a>

<a href="mailto:your@email.com">
  <i class="fas fa-envelope"></i> Email
</a>

8. JavaScript Interactions

Smooth Scroll to Anchors

JavaScript for smooth scrolling:

// Select all anchor links
const anchorLinks = document.querySelectorAll('a[href^="#"]');

anchorLinks.forEach(link => {
  link.addEventListener('click', (e) => {
    e.preventDefault();

    const targetId = link.getAttribute('href');
    const targetElement = document.querySelector(targetId);

    if (targetElement) {
      targetElement.scrollIntoView({
        behavior: 'smooth',
        block: 'start'
      });

      // Update URL without jumping
      history.pushState(null, null, targetId);
    }
  });
});

Or use CSS-only approach:

html {
  scroll-behavior: smooth;
}

HTML structure:

<div class="slider">
  <button class="slider-btn slider-prev" aria-label="Previous slide">
  </button>

  <div class="slider-track">
    <div class="slide active">
      <img src="images/slide-1.jpg" alt="Slide 1">
    </div>
    <div class="slide">
      <img src="images/slide-2.jpg" alt="Slide 2">
    </div>
    <div class="slide">
      <img src="images/slide-3.jpg" alt="Slide 3">
    </div>
  </div>

  <button class="slider-btn slider-next" aria-label="Next slide">
  </button>

  <div class="slider-dots">
    <button class="dot active" data-slide="0"></button>
    <button class="dot" data-slide="1"></button>
    <button class="dot" data-slide="2"></button>
  </div>
</div>

JavaScript for slider:

const slides = document.querySelectorAll('.slide');
const dots = document.querySelectorAll('.dot');
const prevBtn = document.querySelector('.slider-prev');
const nextBtn = document.querySelector('.slider-next');

let currentSlide = 0;

function showSlide(index) {
  // Hide all slides
  slides.forEach(slide => slide.classList.remove('active'));
  dots.forEach(dot => dot.classList.remove('active'));

  // Wrap around if needed
  if (index >= slides.length) {
    currentSlide = 0;
  } else if (index < 0) {
    currentSlide = slides.length - 1;
  } else {
    currentSlide = index;
  }

  // Show current slide
  slides[currentSlide].classList.add('active');
  dots[currentSlide].classList.add('active');
}

// Next/Previous buttons
nextBtn.addEventListener('click', () => showSlide(currentSlide + 1));
prevBtn.addEventListener('click', () => showSlide(currentSlide - 1));

// Dot navigation
dots.forEach(dot => {
  dot.addEventListener('click', () => {
    showSlide(parseInt(dot.dataset.slide));
  });
});

// Auto-advance (optional)
setInterval(() => {
  showSlide(currentSlide + 1);
}, 5000); // Change slide every 5 seconds

9. Performance: Lazy Loading

Load images only when they enter the viewport.

Native Lazy Loading

Simple approach (modern browsers):

<img
  src="project-1.jpg"
  alt="Project screenshot"
  loading="lazy"
>

For multiple images:

<!-- Eager load (above fold) -->
<img src="hero.jpg" alt="Hero" loading="eager">

<!-- Lazy load (below fold) -->
<img src="project-1.jpg" alt="Project 1" loading="lazy">
<img src="project-2.jpg" alt="Project 2" loading="lazy">
<img src="project-3.jpg" alt="Project 3" loading="lazy">

Intersection Observer (Advanced)

HTML with placeholder:

<img
  class="lazy"
  src="placeholder.jpg"
  data-src="actual-image.jpg"
  alt="Project screenshot"
>

JavaScript for lazy loading:

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

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      img.classList.add('loaded');
      observer.unobserve(img);
    }
  });
});

lazyImages.forEach(image => {
  imageObserver.observe(image);
});

10. Practical Exercises

Exercise 4.1: Optimize Images

Optimize all portfolio images:

  1. Convert photos to WebP format
  2. Create 3 sizes (400px, 800px, 1200px)
  3. Compress to under 200 KB each
  4. Test file sizes before/after

Exercise 4.2: Implement Responsive Images

Add responsive images to project pages:

  1. Use srcset for project thumbnails
  2. Use <picture> for hero images
  3. Provide WebP with JPG fallback
  4. Test on different devices/browsers

Create a project screenshot gallery:

  1. Grid layout with 6+ images
  2. Hover effects on gallery items
  3. Modal viewer for full-size images
  4. Keyboard navigation (arrow keys, Escape)

Exercise 4.4: Add Video Demo

Embed a project demonstration video:

  1. Create/embed a demo video
  2. Make it responsive (16:9 ratio)
  3. Add poster image
  4. Ensure controls work on mobile

Exercise 4.5: Implement Lazy Loading

Add lazy loading to improve performance:

  1. Use loading="lazy" on all images below fold
  2. Test with browser DevTools Network tab
  3. Verify images load only when scrolling
  4. Measure performance improvement

11. Knowledge Check

Question 1: When should you use WebP vs JPG?

Show answer Use WebP as primary format (25-35% smaller) with JPG fallback for older browsers. WebP supports both lossy/lossless compression and transparency, making it versatile.

Question 2: What's the purpose of the srcset attribute?

Show answer `srcset` provides multiple image sources at different resolutions, allowing browsers to choose the best image based on screen size and pixel density, improving performance and quality.

Question 3: Why should autoplay videos be muted?

Show answer Most browsers block autoplay with sound to prevent annoying users. Videos can only autoplay if muted. Also better for accessibility and user experience.

Question 4: What's the benefit of lazy loading images?

Show answer Lazy loading defers loading of off-screen images until user scrolls to them, reducing initial page load time, bandwidth usage, and improving performance metrics.

Question 5: What's the difference between <img> and <picture>?

Show answer `` with `srcset` is for resolution switching (same image, different sizes). `` is for art direction (different crops/formats for different contexts).

12. Common Media Mistakes

Unoptimized Images

Bad (huge file):

<img src="photo-from-camera-5MB.jpg" alt="Project">
<!-- 5 MB file! Page loads slowly -->

Good (optimized):

<img
  src="photo-optimized-150kb.webp"
  alt="Project screenshot"
  loading="lazy"
>
<!-- 150 KB, WebP format, lazy loaded -->

Missing Alt Text

Bad:

<img src="project.jpg">  ✗ No alt text
<img src="project.jpg" alt="">  ✗ Empty (unless decorative)
<img src="project.jpg" alt="image">  ✗ Not descriptive

Good:

<img src="project.jpg" alt="E-commerce homepage with product grid">

Non-Responsive Videos

Bad:

<iframe width="800" height="450" src="..."></iframe>
<!-- Fixed size, breaks on mobile -->

Good:

<div class="video-wrapper">
  <iframe src="..."></iframe>
</div>
<!-- Responsive 16:9 wrapper -->

13. Media Checklist

Images:

□ All images optimized (under 200 KB for photos)
□ WebP format with fallbacks
□ Responsive images with srcset
□ Descriptive alt text on all images
□ Lazy loading on below-fold images
□ SVG for logos and icons

Video:

□ Multiple formats (MP4 + WebM)
□ Poster images for videos
□ Responsive video containers
□ Autoplay only if muted
□ Controls visible and accessible

Performance:

□ Images compressed and optimized
□ Lazy loading implemented
□ Critical images loaded first
□ No unnecessarily large files
□ Fast loading on slow connections

Accessibility:

□ All images have alt text
□ Videos have captions/transcripts
□ Keyboard navigation works
□ Focus states visible
□ ARIA labels where needed

14. Key Takeaways

  • Optimize images - Compress and use modern formats (WebP)
  • Responsive images - Use srcset and picture elements
  • Choose correct format - WebP/JPG for photos, SVG for icons
  • Lazy load - Use loading="lazy" for below-fold images
  • Alt text required - Descriptive text for all images
  • Responsive video - Use wrapper div for 16:9 aspect ratio
  • Autoplay must be muted - Browsers block autoplay with sound
  • Test performance - Check file sizes and load times
  • Accessibility matters - Keyboard navigation, captions, alt text
  • Progressive enhancement - Fallbacks for older browsers

15. Further Resources

Image Optimization:

Responsive Images:

Video:

Performance:


Next Steps

Great work! You've learned how to add optimized media and interactive elements to create an engaging, performant portfolio.

In Lesson 5: Testing & Debugging, you'll learn how to test across browsers and devices, validate your code, debug issues, and ensure your site is production-ready.