Pure TailwindMIT licensed

Tailwind Filmstrip Carousel Gallery with CSS Scroll-Snap

A low-profile strip of thumbnails that scrolls and snaps like a camera filmstrip, sprocket holes included. Tailwind ships the whole snap family as utilities, so the carousel — normally a library — is five classes on a container and one on each frame.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tgl-11 w-full min-h-svh grid place-items-center bg-tgl11-bg font-tgl11-sans py-10" aria-label="Filmstrip carousel gallery">
  <div tabindex="0" role="region" aria-label="Scrollable filmstrip, use arrow keys" class="flex gap-2.5 w-full px-5 py-6 overflow-x-auto snap-x snap-mandatory scroll-smooth bg-tgl11-film bg-[repeating-linear-gradient(90deg,#0a0a0a_0_14px,#1c1c1c_14px_22px,#0a0a0a_22px_36px),repeating-linear-gradient(90deg,#0a0a0a_0_14px,#1c1c1c_14px_22px,#0a0a0a_22px_36px)] [background-size:36px_12px,36px_12px] [background-position:0_6px,0_calc(100%-6px)] bg-repeat-x focus-visible:outline-3 focus-visible:outline-tgl11-acc focus-visible:-outline-offset-[3px] motion-reduce:[scroll-behavior:auto]">
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1483058712412-4245e9b90334?q=80&w=500&h=340&auto=format&fit=crop" alt="Market street in Lisbon" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=500&h=340&auto=format&fit=crop" alt="Backstreet in the old town" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1505142468610-359e7d316be0?q=80&w=500&h=340&auto=format&fit=crop" alt="Hillside in early summer" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1511499767150-a48a237f0083?q=80&w=500&h=340&auto=format&fit=crop" alt="Concrete stairwell" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1457369804613-52c61a468e7d?q=80&w=500&h=340&auto=format&fit=crop" alt="Lunch at a roadside table" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1439066615861-d1af74d74000?q=80&w=500&h=340&auto=format&fit=crop" alt="Empty beach at low tide" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=500&h=340&auto=format&fit=crop" alt="Fruit stall at the market" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1518495973542-4542c06a5843?q=80&w=500&h=340&auto=format&fit=crop" alt="Pass road above the treeline" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tgl11-bg: #111111;
  --color-tgl11-bg: oklch(0.18 0 0);
  --color-tgl11-film: #0a0a0a;
  --color-tgl11-film: oklch(0.13 0 0);
  --color-tgl11-frame: #e8e8e8;
  --color-tgl11-frame: oklch(0.93 0 0);
  --color-tgl11-acc: #d8b23c;
  --color-tgl11-acc: oklch(0.78 0.14 90);
  --font-tgl11-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-11 {
  width: 100%;
  min-height: 100svh;
}
/* Scrollbar styling has no variant form — it stays CSS. */

.tgl-11 [role="region"] {
  scrollbar-width: thin;
  scrollbar-color: var(--color-tgl11-acc) #222;
}

.tgl-11 [role="region"]::-webkit-scrollbar {
  height: 8px;
}

.tgl-11 [role="region"]::-webkit-scrollbar-thumb {
  background: var(--color-tgl11-acc);
  border-radius: 8px;
}

.tgl-11 ::selection {
  background: var(--color-tgl11-acc);
  color: #111;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tgl11-bg: #111111;
  --color-tgl11-bg: oklch(0.18 0 0);
  --color-tgl11-film: #0a0a0a;
  --color-tgl11-film: oklch(0.13 0 0);
  --color-tgl11-frame: #e8e8e8;
  --color-tgl11-frame: oklch(0.93 0 0);
  --color-tgl11-acc: #d8b23c;
  --color-tgl11-acc: oklch(0.78 0.14 90);
  --font-tgl11-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-11 {
  width: 100%;
  min-height: 100svh;
}
/* Scrollbar styling has no variant form — it stays CSS. */

.tgl-11 [role="region"] {
  scrollbar-width: thin;
  scrollbar-color: var(--color-tgl11-acc) #222;
}

.tgl-11 [role="region"]::-webkit-scrollbar {
  height: 8px;
}

.tgl-11 [role="region"]::-webkit-scrollbar-thumb {
  background: var(--color-tgl11-acc);
  border-radius: 8px;
}

.tgl-11 ::selection {
  background: var(--color-tgl11-acc);
  color: #111;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The snap family is the clearest case in the collection of Tailwind shipping exactly the right utilities: scroll-snap is a design decision made per element, and every part of it has a class.
  • The sprocket gradients stay inline as arbitrary values even though they are long, because they belong to this strip alone; a @theme entry would imply reuse that will never happen.
  • UI change from the CSS original: alt text is now specific per frame instead of "Travel photo 4", and the hover restores contrast as well as colour so the change reads on a monochrome display too.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Filmstrip Carousel Gallery with CSS Scroll-Snap
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-filmstrip-carousel-gallery-scroll-snap/

A low-profile strip of thumbnails that scrolls and snaps like a camera filmstrip, sprocket holes included. Tailwind ships the whole snap family as utilities, so the carousel — normally a library — is five classes on a container and one on each frame.
## HTML
```html
<section class="tgl-11 w-full min-h-svh grid place-items-center bg-tgl11-bg font-tgl11-sans py-10" aria-label="Filmstrip carousel gallery">
  <div tabindex="0" role="region" aria-label="Scrollable filmstrip, use arrow keys" class="flex gap-2.5 w-full px-5 py-6 overflow-x-auto snap-x snap-mandatory scroll-smooth bg-tgl11-film bg-[repeating-linear-gradient(90deg,#0a0a0a_0_14px,#1c1c1c_14px_22px,#0a0a0a_22px_36px),repeating-linear-gradient(90deg,#0a0a0a_0_14px,#1c1c1c_14px_22px,#0a0a0a_22px_36px)] [background-size:36px_12px,36px_12px] [background-position:0_6px,0_calc(100%-6px)] bg-repeat-x focus-visible:outline-3 focus-visible:outline-tgl11-acc focus-visible:-outline-offset-[3px] motion-reduce:[scroll-behavior:auto]">
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1483058712412-4245e9b90334?q=80&w=500&h=340&auto=format&fit=crop" alt="Market street in Lisbon" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=500&h=340&auto=format&fit=crop" alt="Backstreet in the old town" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1505142468610-359e7d316be0?q=80&w=500&h=340&auto=format&fit=crop" alt="Hillside in early summer" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1511499767150-a48a237f0083?q=80&w=500&h=340&auto=format&fit=crop" alt="Concrete stairwell" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1457369804613-52c61a468e7d?q=80&w=500&h=340&auto=format&fit=crop" alt="Lunch at a roadside table" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1439066615861-d1af74d74000?q=80&w=500&h=340&auto=format&fit=crop" alt="Empty beach at low tide" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=500&h=340&auto=format&fit=crop" alt="Fruit stall at the market" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
    <figure class="group m-0 flex-[0_0_auto] w-[min(340px,72vw)] snap-center border-[3px] border-tgl11-frame rounded-sm bg-tgl11-frame overflow-hidden"><img src="https://images.unsplash.com/photo-1518495973542-4542c06a5843?q=80&w=500&h=340&auto=format&fit=crop" alt="Pass road above the treeline" loading="lazy" decoding="async" class="block w-full [aspect-ratio:3/2] object-cover grayscale-[0.1] contrast-[1.02] transition-[filter] duration-300 group-hover:grayscale-0 group-hover:contrast-100 motion-reduce:transition-none"></figure>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tgl11-bg: #111111;
  --color-tgl11-bg: oklch(0.18 0 0);
  --color-tgl11-film: #0a0a0a;
  --color-tgl11-film: oklch(0.13 0 0);
  --color-tgl11-frame: #e8e8e8;
  --color-tgl11-frame: oklch(0.93 0 0);
  --color-tgl11-acc: #d8b23c;
  --color-tgl11-acc: oklch(0.78 0.14 90);
  --font-tgl11-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-11 {
  width: 100%;
  min-height: 100svh;
}
/* Scrollbar styling has no variant form — it stays CSS. */

.tgl-11 [role="region"] {
  scrollbar-width: thin;
  scrollbar-color: var(--color-tgl11-acc) #222;
}

.tgl-11 [role="region"]::-webkit-scrollbar {
  height: 8px;
}

.tgl-11 [role="region"]::-webkit-scrollbar-thumb {
  background: var(--color-tgl11-acc);
  border-radius: 8px;
}

.tgl-11 ::selection {
  background: var(--color-tgl11-acc);
  color: #111;
}
```

How this works

Snap is a design-scale feature, so Tailwind ships it. snap-x, snap-mandatory and snap-center map one to one onto the CSS, and the result is a carousel with native momentum, native keyboard scrolling and no library:

<div tabindex="0" role="region" class="flex gap-2.5 overflow-x-auto snap-x snap-mandatory">

The film is decoration, and it stays in the class list. Two repeating gradients pinned top and bottom paint the sprocket holes; the sizes and positions are arbitrary properties because 36×12px is film geometry, not a spacing scale.

Scrollbar styling is the one thing that must stay CSS. ::-webkit-scrollbar has no variant form, so it lives in the stylesheet with scrollbar-width/scrollbar-color beside it.

Make it yours

  • Frame size: w-[min(340px,72vw)] on each frame — smaller frames show more strip.
  • Alignment: snap-start for a left-aligned feed instead of centred.
  • Delete the two gradient utilities for a clean modern thumbnail rail with the same snapping.

Gotchas — read before shipping

  • A scroll container needs tabindex="0" and a visible focus style, or keyboard users cannot scroll it at all — this is the most-missed part of every snap carousel.
  • snap-mandatory is right for a short horizontal strip; use snap-proximity for long or variable-height feeds.
  • Fix the frame width so lazy-loaded images cannot collapse the strip mid-scroll.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

CSS scroll-snap is universal; the floor is Tailwind v4's generated sheet. ::scroll-button controls are progressive enhancement (Chrome 135+).

Techniques used in this demo

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…