0Support

CSS carousel without JavaScript

Combine scroll snap with ::scroll-button(), ::scroll-marker, and ::scroll-marker-group to explore a native CSS carousel. Compare links and tabs modes, navigation, accessibility and fallback in the guide.

Read the guide

Ask AI

CSS carousel without JavaScript

Response generated with openai/gpt-5.4-nano. AI can make mistakes. Always review the result.

Drag, use the wheel, keyboard, or the controls generated by the browser.

Your browser does not support the selected mode. Positional markers retain this version’s own behavior; the code shows the requested mode.

Your browser does not render the experimental buttons or markers yet. Scrolling and scroll snap remain available as the fallback.

HTML
<ul class="carousel" aria-label="Native CSS carousel examples" tabindex="0">
    <li
      class="slide"
      tabindex="-1"
      data-marker-label="A reliable foundation"
      style="--slide-accent: #ffb38a"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">01 · Scroll snap</p>
        <h2>A reliable foundation</h2>
        <p>The scroll port settles each card into a predictable position.</p>
      </article>
    </li>
    <li
      class="slide"
      tabindex="-1"
      data-marker-label="Controls from CSS"
      style="--slide-accent: #72d6c9"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">02 · Scroll buttons</p>
        <h2>Controls from CSS</h2>
        <p>The browser creates real previous and next buttons without extra markup.</p>
      </article>
    </li>
    <li
      class="slide"
      tabindex="-1"
      data-marker-label="Native pagination"
      style="--slide-accent: #b6a8ff"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">03 · Scroll markers</p>
        <h2>Native pagination</h2>
        <p>Every slide generates a marker that navigates to its own scroll target.</p>
      </article>
    </li>
    <li
      class="slide"
      tabindex="-1"
      data-marker-label="State without scripts"
      style="--slide-accent: #f7a6c4"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">04 · Target current</p>
        <h2>State without scripts</h2>
        <p>The current marker follows the visible slide through :target-current.</p>
      </article>
    </li>
</ul>
Tailwind CSS
Some classes with arbitrary values can be replaced by native classes, depending on the value and your Tailwind CSS configuration.
<ul class="flex flex-row gap-[16px] [inline-size:min(100%,_56rem)] m-0 p-0 overflow-x-auto [overscroll-behavior-inline:contain] scroll-smooth motion-reduce:scroll-auto [scroll-snap-type:inline_mandatory] [scroll-marker-group:after] supports-[scroll-marker-group:after_links]:[scroll-marker-group:after_links] [anchor-name:--carousel] [&::scroll-marker-group]:flex [&::scroll-marker-group]:flex-row [&::scroll-marker-group]:items-center [&::scroll-marker-group]:justify-center [&::scroll-marker-group]:gap-3 [&::scroll-marker-group]:pt-5 [&::scroll-marker-group]:pb-2 [&::scroll-button(*)]:absolute [&::scroll-button(*)]:[position-anchor:--carousel] [&::scroll-button(*)]:z-[1] [&::scroll-button(*)]:grid [&::scroll-button(*)]:place-items-center [&::scroll-button(*)]:size-10 [&::scroll-button(*)]:border-0 [&::scroll-button(*)]:rounded-full [&::scroll-button(*)]:bg-[#172033] [&::scroll-button(*)]:text-white [&::scroll-button(*)]:[font:inherit] [&::scroll-button(*)]:text-[1.15rem] [&::scroll-button(*)]:shadow-[0_0.75rem_2rem_rgb(0_0_0/0.24)] [&::scroll-button(*)]:cursor-pointer [&::scroll-button(*)]:transition-[background-color,opacity,scale] [&::scroll-button(*)]:duration-200 [&::scroll-button(*):hover]:bg-[#7c5cff] [&::scroll-button(*):hover]:scale-[1.08] [&::scroll-button(*):focus-visible]:[outline:3px_solid_color-mix(in_oklch,_#7c5cff_42%,_transparent)] [&::scroll-button(*):focus-visible]:outline-offset-3 [&::scroll-button(*):disabled]:opacity-[0.35] [&::scroll-button(*):disabled]:cursor-not-allowed [&::scroll-button(inline-start)]:content-['←'] [&::scroll-button(inline-start)]:[position-area:left_center] [&::scroll-button(inline-start)]:[translate:60%] [&::scroll-button(inline-end)]:content-['→'] [&::scroll-button(inline-end)]:[position-area:right_center] [&::scroll-button(inline-end)]:[translate:-60%]" aria-label="Native CSS carousel examples" tabindex="0">
  <li class="list-none flex-[0_0_84%] [scroll-snap-align:center] [scroll-snap-stop:always] [&::scroll-marker]:[content:''_/_attr(data-marker-label)] [&::scroll-marker]:[inline-size:20px] [&::scroll-marker]:[block-size:20px] [&::scroll-marker]:border-2 [&::scroll-marker]:border-[#64748b] [&::scroll-marker]:rounded-full [&::scroll-marker]:bg-white [&::scroll-marker]:cursor-pointer [&::scroll-marker]:transition-[background-color,border-color,scale] [&::scroll-marker]:duration-200 [&::scroll-marker:hover]:border-[#7c5cff] [&::scroll-marker:hover]:bg-[color-mix(in_oklch,#7c5cff_18%,#fff)] [&::scroll-marker:focus-visible]:[outline:3px_solid_color-mix(in_oklch,_#7c5cff_38%,_transparent)] [&::scroll-marker:focus-visible]:outline-offset-2 [&::scroll-marker:target-current]:border-[#172033] [&::scroll-marker:target-current]:bg-[#172033] [&::scroll-marker:target-current]:shadow-[0_0_0_2px_#fff,0_0_0_4px_#172033] [&::scroll-marker:target-current]:scale-110" tabindex="-1" data-marker-label="A reliable foundation">
    <article class="box-border grid content-end gap-2 min-block-[18rem] p-[clamp(1.5rem,5vw,3rem)] border border-[color-mix(in_oklch,#ffb38a_72%,#172033)] rounded-[1.5rem] bg-[#ffb38a] text-[#172033] *:m-0 [&_h2]:max-inline-[16ch] [&_h2]:text-[clamp(1.75rem,6vw,3.5rem)] [&_h2]:leading-none [&_h2]:text-balance">
      <p class="text-[rgb(23_32_51/0.72)] font-bold tracking-[0.08em] uppercase">01 · Scroll snap</p>
      <h2>A reliable foundation</h2>
      <p>The scroll port settles each card into a predictable position.</p>
    </article>
  </li>
  <li class="list-none flex-[0_0_84%] [scroll-snap-align:center] [scroll-snap-stop:always] [&::scroll-marker]:[content:''_/_attr(data-marker-label)] [&::scroll-marker]:[inline-size:20px] [&::scroll-marker]:[block-size:20px] [&::scroll-marker]:border-2 [&::scroll-marker]:border-[#64748b] [&::scroll-marker]:rounded-full [&::scroll-marker]:bg-white [&::scroll-marker]:cursor-pointer [&::scroll-marker]:transition-[background-color,border-color,scale] [&::scroll-marker]:duration-200 [&::scroll-marker:hover]:border-[#7c5cff] [&::scroll-marker:hover]:bg-[color-mix(in_oklch,#7c5cff_18%,#fff)] [&::scroll-marker:focus-visible]:[outline:3px_solid_color-mix(in_oklch,_#7c5cff_38%,_transparent)] [&::scroll-marker:focus-visible]:outline-offset-2 [&::scroll-marker:target-current]:border-[#172033] [&::scroll-marker:target-current]:bg-[#172033] [&::scroll-marker:target-current]:shadow-[0_0_0_2px_#fff,0_0_0_4px_#172033] [&::scroll-marker:target-current]:scale-110" tabindex="-1" data-marker-label="Controls from CSS">
    <article class="box-border grid content-end gap-2 min-block-[18rem] p-[clamp(1.5rem,5vw,3rem)] border border-[color-mix(in_oklch,#72d6c9_72%,#172033)] rounded-[1.5rem] bg-[#72d6c9] text-[#172033] *:m-0 [&_h2]:max-inline-[16ch] [&_h2]:text-[clamp(1.75rem,6vw,3.5rem)] [&_h2]:leading-none [&_h2]:text-balance">
      <p class="text-[rgb(23_32_51/0.72)] font-bold tracking-[0.08em] uppercase">02 · Scroll buttons</p>
      <h2>Controls from CSS</h2>
      <p>The browser creates real previous and next buttons without extra markup.</p>
    </article>
  </li>
  <li class="list-none flex-[0_0_84%] [scroll-snap-align:center] [scroll-snap-stop:always] [&::scroll-marker]:[content:''_/_attr(data-marker-label)] [&::scroll-marker]:[inline-size:20px] [&::scroll-marker]:[block-size:20px] [&::scroll-marker]:border-2 [&::scroll-marker]:border-[#64748b] [&::scroll-marker]:rounded-full [&::scroll-marker]:bg-white [&::scroll-marker]:cursor-pointer [&::scroll-marker]:transition-[background-color,border-color,scale] [&::scroll-marker]:duration-200 [&::scroll-marker:hover]:border-[#7c5cff] [&::scroll-marker:hover]:bg-[color-mix(in_oklch,#7c5cff_18%,#fff)] [&::scroll-marker:focus-visible]:[outline:3px_solid_color-mix(in_oklch,_#7c5cff_38%,_transparent)] [&::scroll-marker:focus-visible]:outline-offset-2 [&::scroll-marker:target-current]:border-[#172033] [&::scroll-marker:target-current]:bg-[#172033] [&::scroll-marker:target-current]:shadow-[0_0_0_2px_#fff,0_0_0_4px_#172033] [&::scroll-marker:target-current]:scale-110" tabindex="-1" data-marker-label="Native pagination">
    <article class="box-border grid content-end gap-2 min-block-[18rem] p-[clamp(1.5rem,5vw,3rem)] border border-[color-mix(in_oklch,#b6a8ff_72%,#172033)] rounded-[1.5rem] bg-[#b6a8ff] text-[#172033] *:m-0 [&_h2]:max-inline-[16ch] [&_h2]:text-[clamp(1.75rem,6vw,3.5rem)] [&_h2]:leading-none [&_h2]:text-balance">
      <p class="text-[rgb(23_32_51/0.72)] font-bold tracking-[0.08em] uppercase">03 · Scroll markers</p>
      <h2>Native pagination</h2>
      <p>Every slide generates a marker that navigates to its own scroll target.</p>
    </article>
  </li>
  <li class="list-none flex-[0_0_84%] [scroll-snap-align:center] [scroll-snap-stop:always] [&::scroll-marker]:[content:''_/_attr(data-marker-label)] [&::scroll-marker]:[inline-size:20px] [&::scroll-marker]:[block-size:20px] [&::scroll-marker]:border-2 [&::scroll-marker]:border-[#64748b] [&::scroll-marker]:rounded-full [&::scroll-marker]:bg-white [&::scroll-marker]:cursor-pointer [&::scroll-marker]:transition-[background-color,border-color,scale] [&::scroll-marker]:duration-200 [&::scroll-marker:hover]:border-[#7c5cff] [&::scroll-marker:hover]:bg-[color-mix(in_oklch,#7c5cff_18%,#fff)] [&::scroll-marker:focus-visible]:[outline:3px_solid_color-mix(in_oklch,_#7c5cff_38%,_transparent)] [&::scroll-marker:focus-visible]:outline-offset-2 [&::scroll-marker:target-current]:border-[#172033] [&::scroll-marker:target-current]:bg-[#172033] [&::scroll-marker:target-current]:shadow-[0_0_0_2px_#fff,0_0_0_4px_#172033] [&::scroll-marker:target-current]:scale-110" tabindex="-1" data-marker-label="State without scripts">
    <article class="box-border grid content-end gap-2 min-block-[18rem] p-[clamp(1.5rem,5vw,3rem)] border border-[color-mix(in_oklch,#f7a6c4_72%,#172033)] rounded-[1.5rem] bg-[#f7a6c4] text-[#172033] *:m-0 [&_h2]:max-inline-[16ch] [&_h2]:text-[clamp(1.75rem,6vw,3.5rem)] [&_h2]:leading-none [&_h2]:text-balance">
      <p class="text-[rgb(23_32_51/0.72)] font-bold tracking-[0.08em] uppercase">04 · Target current</p>
      <h2>State without scripts</h2>
      <p>The current marker follows the visible slide through :target-current.</p>
    </article>
  </li>
</ul>
CSS
.carousel {
  display: flex;
  flex-direction: row;
  gap: 16px;
  inline-size: min(100%, 56rem);
  margin: 0;
  padding: 0;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-behavior: smooth;
  scroll-snap-type: inline mandatory;
  scroll-marker-group: after;
  scroll-marker-group: after links;
  anchor-name: --carousel;
}

.carousel > .slide {
  list-style: none;
  flex: 0 0 84%;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

.slide-card {
  box-sizing: border-box;
  display: grid;
  align-content: end;
  gap: 0.5rem;
  min-block-size: 18rem;
  padding: clamp(1.5rem, 5vw, 3rem);
  border: 1px solid color-mix(in oklch, var(--slide-accent) 72%, #172033);
  border-radius: 1.5rem;
  background: var(--slide-accent);
  color: #172033;
}

.slide-card > * {
  margin: 0;
}

.slide-card h2 {
  max-inline-size: 16ch;
  font-size: clamp(1.75rem, 6vw, 3.5rem);
  line-height: 1;
  text-wrap: balance;
}

.slide-eyebrow {
  color: rgb(23 32 51 / 0.72);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.carousel::scroll-marker-group {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding-block: 1.25rem 0.5rem;
}

.carousel > .slide::scroll-marker {
  content: "" / attr(data-marker-label);
  inline-size: 20px;
  block-size: 20px;
  border: 2px solid #64748b;
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s, scale 0.2s;
}

.carousel > .slide::scroll-marker:hover {
  border-color: #7c5cff;
  background: color-mix(in oklch, #7c5cff 18%, #fff);
}

.carousel > .slide::scroll-marker:focus-visible {
  outline: 3px solid color-mix(in oklch, #7c5cff 38%, transparent);
  outline-offset: 2px;
}

.carousel > .slide::scroll-marker:target-current {
  border-color: #172033;
  background: #172033;
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px #172033;
  scale: 1.1;
}

.carousel::scroll-button(*) {
  position: absolute;
  position-anchor: --carousel;
  z-index: 1;
  display: grid;
  place-items: center;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: #172033;
  color: #fff;
  font: inherit;
  font-size: 1.15rem;
  box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.24);
  cursor: pointer;
  transition: background-color 0.2s, opacity 0.2s, scale 0.2s;
}

.carousel::scroll-button(*):hover {
  background: #7c5cff;
  scale: 1.08;
}

.carousel::scroll-button(*):focus-visible {
  outline: 3px solid color-mix(in oklch, #7c5cff 42%, transparent);
  outline-offset: 3px;
}

.carousel::scroll-button(*):disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.carousel::scroll-button(inline-start) {
  content: "←";
  position-area: left center;
  translate: 60%;
}

.carousel::scroll-button(inline-end) {
  content: "→";
  position-area: right center;
  translate: -60%;
}

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

Educational guide to CSS carousel

From a scrolling list to a carousel with navigation

A carousel combines HTML content, resting positions, and controls for moving through it. Learn to add native buttons and markers while preserving reading and navigation when those features are unavailable.

Back to the playground