CSS carousel without JavaScript

Combine scroll snap with ::scroll-button(), ::scroll-marker, and ::scroll-marker-group to build a native, accessible slider controlled entirely by CSS.

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 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"
      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"
      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"
      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"
      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] [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-[''] [&::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">
    <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-[''] [&::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">
    <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-[''] [&::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">
    <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-[''] [&::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">
    <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;
  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: "";
  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;
  }
}