Una base predecible
El área de scroll encaja cada tarjeta en una posición estable.
Combina scroll snap con ::scroll-button(), ::scroll-marker y ::scroll-marker-group para crear un slider nativo, accesible y controlado solo con CSS.
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.
<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><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>.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;
}
}.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;
> .slide {
list-style: none;
flex: 0 0 84%;
scroll-snap-align: center;
scroll-snap-stop: always;
}
&::scroll-marker-group {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding-block: 1.25rem 0.5rem;
}
> .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;
}
> .slide::scroll-marker:hover {
border-color: #7c5cff;
background: color-mix(in oklch, #7c5cff 18%, #fff);
}
> .slide::scroll-marker:focus-visible {
outline: 3px solid color-mix(in oklch, #7c5cff 38%, transparent);
outline-offset: 2px;
}
> .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;
}
&::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;
}
&::scroll-button(*):hover {
background: #7c5cff;
scale: 1.08;
}
&::scroll-button(*):focus-visible {
outline: 3px solid color-mix(in oklch, #7c5cff 42%, transparent);
outline-offset: 3px;
}
&::scroll-button(*):disabled {
opacity: 0.35;
cursor: not-allowed;
}
&::scroll-button(inline-start) {
content: "←";
position-area: left center;
translate: 60%;
}
&::scroll-button(inline-end) {
content: "→";
position-area: right center;
translate: -60%;
}
@media (prefers-reduced-motion: reduce) {
scroll-behavior: auto;
}
}
.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;
> * {
margin: 0;
}
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;
}