Carrusel CSS sin JavaScript

Combina scroll snap con ::scroll-button(), ::scroll-marker y ::scroll-marker-group para crear un slider nativo, accesible y controlado solo con CSS.

Preguntar a la IA

Carrusel CSS sin JavaScript

Respuesta generada con openai/gpt-5.4-nano. La IA puede cometer errores. Revisa siempre el resultado.

Arrastra, usa la rueda, el teclado o los controles generados por el navegador.

Tu navegador todavía no renderiza los botones o marcadores experimentales. El carrusel mantiene el scroll y scroll snap como fallback.

HTML
<ul class="carousel" aria-label="Ejemplos de carrusel nativo con CSS" tabindex="0">
    <li
      class="slide"
      style="--slide-accent: #ffb38a"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">01 · Scroll snap</p>
        <h2>Una base predecible</h2>
        <p>El área de scroll encaja cada tarjeta en una posición estable.</p>
      </article>
    </li>
    <li
      class="slide"
      style="--slide-accent: #72d6c9"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">02 · Scroll buttons</p>
        <h2>Controles desde CSS</h2>
        <p>El navegador crea botones anterior y siguiente sin añadir markup.</p>
      </article>
    </li>
    <li
      class="slide"
      style="--slide-accent: #b6a8ff"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">03 · Scroll markers</p>
        <h2>Paginación nativa</h2>
        <p>Cada slide genera un marcador que navega hasta su propio destino.</p>
      </article>
    </li>
    <li
      class="slide"
      style="--slide-accent: #f7a6c4"
    >
      <article class="slide-card">
        <p class="slide-eyebrow">04 · Target current</p>
        <h2>Estado sin scripts</h2>
        <p>El marcador actual sigue al slide visible mediante :target-current.</p>
      </article>
    </li>
</ul>
Tailwind CSS
Algunas clases con valores arbitrarios pueden sustituirse por clases nativas, según el valor y la configuración de Tailwind CSS.
<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="Ejemplos de carrusel nativo con CSS" 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>Una base predecible</h2>
      <p>El área de scroll encaja cada tarjeta en una posición estable.</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>Controles desde CSS</h2>
      <p>El navegador crea botones anterior y siguiente sin añadir 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>Paginación nativa</h2>
      <p>Cada slide genera un marcador que navega hasta su propio destino.</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>Estado sin scripts</h2>
      <p>El marcador actual sigue al slide visible mediante :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;
  }
}