Pure TailwindMIT licensed

Tailwind Portfolio Gallery Slider with Hover Caption Reveal

A case-study rail where each screenshot reveals its title and category on hover, sliding up from the bottom edge. Two transforms on two elements, both driven from one group on the link — and the caption is real text, so it is indexable as well as visible.

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="tcar-08 w-full min-h-svh grid place-items-center bg-tcar08-bg font-tcar08-sans px-3 py-10" aria-label="Selected work">
  <ul tabindex="0" role="group" aria-label="Portfolio projects, scrollable" class="list-none flex gap-[18px] w-[min(820px,100%)] overflow-x-auto p-3.5 m-0 snap-x snap-mandatory [scroll-padding:14px] [scrollbar-width:thin] [scrollbar-color:var(--color-tcar08-track)_transparent] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2 focus-visible:rounded-2xl">
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-1/640/800" width="640" height="800" decoding="async" alt="Fintech dashboard redesign" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Product</span><span class="block text-base font-bold text-white">Ledger &mdash; Fintech Dashboard</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-2/640/800" width="640" height="800" loading="lazy" decoding="async" alt="Coffee brand identity" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Branding</span><span class="block text-base font-bold text-white">C&ugrave;rrent &mdash; Coffee Identity</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-3/640/800" width="640" height="800" loading="lazy" decoding="async" alt="Travel app case study" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Mobile</span><span class="block text-base font-bold text-white">Wander &mdash; Travel App</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-4/640/800" width="640" height="800" loading="lazy" decoding="async" alt="E-commerce replatform" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Web</span><span class="block text-base font-bold text-white">Maison &mdash; E-commerce</span>
        </span>
      </a>
    </li>
  </ul>
</section>
@import "tailwindcss";

@theme {
  --color-tcar08-bg: #111318;
  --color-tcar08-bg: oklch(0.19 0.008 265);
  --color-tcar08-card: #1a1d24;
  --color-tcar08-card: oklch(0.24 0.01 265);
  --color-tcar08-track: #333842;
  --color-tcar08-track: oklch(0.34 0.012 265);
  --color-tcar08-acc: #e6ff4d;
  --color-tcar08-acc: oklch(0.94 0.19 115);
  --font-tcar08-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tcar-08 {
  width: 100%;
  min-height: 100svh;
}

.tcar-08 ::selection {
  background: var(--color-tcar08-acc);
  color: #111;
}

@media (prefers-reduced-motion: reduce) {
  .tcar-08 ul {
    scroll-behavior: auto;
  }
}

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-tcar08-bg: #111318;
  --color-tcar08-bg: oklch(0.19 0.008 265);
  --color-tcar08-card: #1a1d24;
  --color-tcar08-card: oklch(0.24 0.01 265);
  --color-tcar08-track: #333842;
  --color-tcar08-track: oklch(0.34 0.012 265);
  --color-tcar08-acc: #e6ff4d;
  --color-tcar08-acc: oklch(0.94 0.19 115);
  --font-tcar08-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tcar-08 {
  width: 100%;
  min-height: 100svh;
}

.tcar-08 ::selection {
  background: var(--color-tcar08-acc);
  color: #111;
}

@media (prefers-reduced-motion: reduce) {
  .tcar-08 ul {
    scroll-behavior: auto;
  }
}
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.

  • translate-y-full replaces the original's translateY(101%) hack. The extra 1% existed to hide a subpixel seam; Tailwind's 100% plus the frame's overflow-hidden makes it unnecessary.
  • The reduced-motion branch does not merely disable the transition — it pins the caption open. A caption that animates into view must be visible, not absent, when the animation is off.
  • UI change from the CSS original: the link itself gained focus-visible:outline-3. The original relied on the caption reveal alone to indicate focus, which is invisible to anyone who cannot see the animation and fails WCAG 2.4.7.
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 Portfolio Gallery Slider with Hover Caption Reveal
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-portfolio-gallery-hover-caption-reveal/

A case-study rail where each screenshot reveals its title and category on hover, sliding up from the bottom edge. Two transforms on two elements, both driven from one group on the link — and the caption is real text, so it is indexable as well as visible.
## HTML
```html
<section class="tcar-08 w-full min-h-svh grid place-items-center bg-tcar08-bg font-tcar08-sans px-3 py-10" aria-label="Selected work">
  <ul tabindex="0" role="group" aria-label="Portfolio projects, scrollable" class="list-none flex gap-[18px] w-[min(820px,100%)] overflow-x-auto p-3.5 m-0 snap-x snap-mandatory [scroll-padding:14px] [scrollbar-width:thin] [scrollbar-color:var(--color-tcar08-track)_transparent] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2 focus-visible:rounded-2xl">
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-1/640/800" width="640" height="800" decoding="async" alt="Fintech dashboard redesign" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Product</span><span class="block text-base font-bold text-white">Ledger &mdash; Fintech Dashboard</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-2/640/800" width="640" height="800" loading="lazy" decoding="async" alt="Coffee brand identity" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Branding</span><span class="block text-base font-bold text-white">C&ugrave;rrent &mdash; Coffee Identity</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-3/640/800" width="640" height="800" loading="lazy" decoding="async" alt="Travel app case study" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Mobile</span><span class="block text-base font-bold text-white">Wander &mdash; Travel App</span>
        </span>
      </a>
    </li>
    <li class="flex-none basis-[clamp(220px,72%,260px)] snap-start">
      <a href="#" class="group block relative rounded-2xl overflow-hidden no-underline bg-tcar08-card shadow-[0_20px_40px_-30px_oklch(0_0_0/0.9)] focus-visible:outline-3 focus-visible:outline-tcar08-acc focus-visible:outline-offset-2">
        <span class="block aspect-[4/5] overflow-hidden"><img src="https://picsum.photos/seed/work-4/640/800" width="640" height="800" loading="lazy" decoding="async" alt="E-commerce replatform" class="size-full object-cover block transition-[scale] duration-500 ease-out group-hover:scale-[1.07] group-focus-visible:scale-[1.07] motion-reduce:transition-none motion-reduce:group-hover:scale-100"></span>
        <span class="absolute inset-x-0 bottom-0 px-[18px] pt-11 pb-[18px] bg-[linear-gradient(transparent,oklch(0.14_0.01_265/0.92))] translate-y-full opacity-0 transition-[translate,opacity] duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:translate-y-0 group-focus-within:opacity-100 motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100">
          <span class="inline-block text-[11px] tracking-[0.14em] uppercase text-tcar08-acc mb-1.5">Web</span><span class="block text-base font-bold text-white">Maison &mdash; E-commerce</span>
        </span>
      </a>
    </li>
  </ul>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcar08-bg: #111318;
  --color-tcar08-bg: oklch(0.19 0.008 265);
  --color-tcar08-card: #1a1d24;
  --color-tcar08-card: oklch(0.24 0.01 265);
  --color-tcar08-track: #333842;
  --color-tcar08-track: oklch(0.34 0.012 265);
  --color-tcar08-acc: #e6ff4d;
  --color-tcar08-acc: oklch(0.94 0.19 115);
  --font-tcar08-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tcar-08 {
  width: 100%;
  min-height: 100svh;
}

.tcar-08 ::selection {
  background: var(--color-tcar08-acc);
  color: #111;
}

@media (prefers-reduced-motion: reduce) {
  .tcar-08 ul {
    scroll-behavior: auto;
  }
}
```

How this works

The group is the link, not the card. Putting group on the <a> means group-focus-within: fires when the link itself takes focus, so tabbing through the rail reveals each caption in turn.

Two elements, two speeds. The image scales over 500ms while the caption slides over 400ms on a settle curve. Both animate transform only.

The caption is markup. Category and title are real spans, not content: strings — announced by screen readers and crawled by search engines, which a pseudo-element would not be.

Make it yours

  • Reveal direction: the caption's rest transform (translate-y-full, translate-x-full, or opacity-only).
  • Zoom strength: group-hover:scale-[1.07].
  • Scrim: the arbitrary gradient on the caption.
  • Cards per view: the card's basis-*.

Gotchas — read before shipping

  • overflow-hidden belongs on the image frame, not the card. On the card it clips the caption as well and the reveal never appears.
  • Keep the caption in the DOM. A before:content-['Ledger'] caption is invisible to assistive tech and to crawlers — on a portfolio, that is the page's entire value.
  • group-focus-within: is mandatory alongside group-hover:; without it the project titles are unreachable by keyboard.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:focus-within and scroll-snap are universally supported; the floor quoted is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…