DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Responsive Video Grid with CSS Grid

Displaying a grid of video thumbnails that looks good on all screen sizes is a common frontend challenge. Here's the CSS Grid approach I use at DailyWatch, with auto-fill columns, aspect ratio preservation, and mobile optimization.

The HTML Structure

<div class="video-grid">
  <article class="video-card">
    <a href="/watch/abc123" class="video-card__link">
      <div class="video-card__thumb">
        <img src="https://i.ytimg.com/vi/abc123/mqdefault.jpg"
             alt="Video title here"
             loading="lazy"
             width="320" height="180">
        <span class="video-card__duration">4:32</span>
      </div>
      <div class="video-card__info">
        <h3 class="video-card__title">Amazing Video Title That Might Be Long</h3>
        <p class="video-card__channel">Channel Name</p>
        <p class="video-card__meta">
          <span>1.5M views</span>
          <span>2 days ago</span>
        </p>
      </div>
    </a>
  </article>
  <!-- More cards... -->
</div>
Enter fullscreen mode Exit fullscreen mode

The CSS Grid Layout

.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  padding: 16px;
}

.video-card {
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.15s ease;
}

.video-card:hover {
  transform: translateY(-2px);
}

.video-card__link {
  text-decoration: none;
  color: inherit;
  display: block;
}

.video-card__thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #1a1a2e;
  border-radius: 8px;
}

.video-card__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.video-card__duration {
  position: absolute;
  bottom: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.4;
}

.video-card__info {
  padding: 10px 2px;
}

.video-card__title {
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.3;
  margin: 0 0 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.video-card__channel {
  font-size: 0.85rem;
  color: #aaa;
  margin: 0 0 2px;
}

.video-card__meta {
  font-size: 0.8rem;
  color: #888;
  margin: 0;
}

.video-card__meta span + span::before {
  content: ' \00B7 ';
}
Enter fullscreen mode Exit fullscreen mode

Responsive Breakpoints

/* Mobile: 1 column */
@media (max-width: 480px) {
  .video-grid {
    grid-template-columns: 1fr;
    gap: 16px;
    padding: 12px;
  }
}

/* Small tablet: 2 columns */
@media (min-width: 481px) and (max-width: 768px) {
  .video-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
  }
}

/* Large screens: cap at 5 columns */
@media (min-width: 1600px) {
  .video-grid {
    grid-template-columns: repeat(5, 1fr);
    max-width: 1800px;
    margin: 0 auto;
  }
}
Enter fullscreen mode Exit fullscreen mode

PHP Helper for Generating the Grid

function renderVideoGrid(array $videos, string $class = ''): string {
    $html = '<div class="video-grid ' . htmlspecialchars($class) . '">';

    foreach ($videos as $video) {
        $title = htmlspecialchars($video['title']);
        $channel = htmlspecialchars($video['channel_title']);
        $thumb = htmlspecialchars($video['thumbnail_url']);
        $duration = formatDuration($video['duration'] ?? '');
        $views = formatViewCount($video['view_count'] ?? 0);
        $timeAgo = timeAgo($video['published_at'] ?? '');

        $html .= <<<HTML
        <article class="video-card">
          <a href="/watch/{$video['video_id']}" class="video-card__link">
            <div class="video-card__thumb">
              <img src="{$thumb}" alt="{$title}" loading="lazy" width="320" height="180">
              <span class="video-card__duration">{$duration}</span>
            </div>
            <div class="video-card__info">
              <h3 class="video-card__title">{$title}</h3>
              <p class="video-card__channel">{$channel}</p>
              <p class="video-card__meta">
                <span>{$views} views</span>
                <span>{$timeAgo}</span>
              </p>
            </div>
          </a>
        </article>
        HTML;
    }

    $html .= '</div>';
    return $html;
}

function formatDuration(string $iso): string {
    if (preg_match('/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', $iso, $m)) {
        $h = (int)($m[1] ?? 0);
        $min = (int)($m[2] ?? 0);
        $s = (int)($m[3] ?? 0);

        if ($h > 0) return sprintf('%d:%02d:%02d', $h, $min, $s);
        return sprintf('%d:%02d', $min, $s);
    }
    return '';
}

function formatViewCount(int $count): string {
    if ($count >= 1000000) return round($count / 1000000, 1) . 'M';
    if ($count >= 1000) return round($count / 1000, 1) . 'K';
    return (string)$count;
}
Enter fullscreen mode Exit fullscreen mode

The key insight with auto-fill + minmax is that the browser automatically calculates the optimal number of columns based on available width. No media queries needed for the basic responsive behavior.

This grid layout powers the video listings on dailywatch.video, automatically adapting from 1 column on phones to 4-5 columns on wide desktops without a single JavaScript calculation.

Top comments (0)