Carrusel

La tira que se arrastra, con coverflow atado al scroll y sin una línea de JavaScript. No se importa solo.

Leer la versión en texto src/components/carrusel.css

Qué es

Esta es la única pieza que no entra con components.css, y es deliberado. Sus reglas son globales —.car, .car-celda— y se aplicaban encima del carrusel que la landing ya tenía: dos coverflow superpuestos, y las tarjetas de los lados salían lavadas. Quien lo quiera lo importa explícitamente. Es la diferencia entre publicar una pieza y activarla en todos los que ya tienen la suya.

Funciona sin JavaScript. El encaje lo hace el navegador con scroll-snap: nadie escribe la física, nadie mide posiciones por fotograma. El coverflow va atado a view-timeline, así que el 3D sigue al dedo y al trackpad en tiempo real. Lo único que necesita guion son los dos botones.

Cómo se importa

@import "@pantherkit/design-system/tokens.css";  /* siempre antes */
@import "@pantherkit/design-system/carrusel.css";

El marcado, vivo

Lo que se ve aquí abajo y lo que hay en el bloque de código son el mismo texto: se inyecta y se imprime desde una sola fuente. Si se separaran, el que se copia sería el que nadie mira.

La tira con coverflow

Los mandos van arriba, junto al titular, y no flotando sobre las tarjetas: encima de una captura tapan justo lo que se quiere ver, y ahí no se sabe si el clic abre el proyecto o mueve el carrusel. Arrástrala con el trackpad y mira cómo giran las celdas de los bordes: eso es view-timeline, no un guion.

Proyectos

Plataforma

Uno

La celda fija el ancho; la tarjeta de dentro es la que gira.

Infraestructura

Dos

El giro es hacia dentro, como se apila una baraja. Al revés parece que se caen.

Producto

Tres

Solo la celda que pasa por el centro está de frente: la meseta es un punto, no un tramo.

Datos

Cuatro

Sin JavaScript la tira se arrastra igual. Lo que faltan son los dos botones.

Interno

Cinco

Al llegar al final el botón no desaparece: se apaga.

Los botones necesitan guion —encender, apagar y desplazar—; el resto es CSS. El de la vitrina está debajo.

El marcado
<div class="mb-4 flex items-end justify-between gap-4">
  <h3 class="text-xl font-semibold tracking-[-0.012em]">Proyectos</h3>
  <div class="car-mandos" data-car-mandos="tira-demo">
    <button type="button" class="car-boton" data-car-prev aria-label="Anterior" disabled>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>
    </button>
    <button type="button" class="car-boton" data-car-next aria-label="Siguiente">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
    </button>
  </div>
</div>

<div class="car" id="tira-demo" role="region" aria-label="Proyectos" tabindex="0">
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Plataforma</p>
      <h4 class="mt-2 text-base font-semibold">Uno</h4>
      <p class="mt-2 text-sm text-text-secondary">La celda fija el ancho; la tarjeta de dentro es la que gira.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Infraestructura</p>
      <h4 class="mt-2 text-base font-semibold">Dos</h4>
      <p class="mt-2 text-sm text-text-secondary">El giro es hacia dentro, como se apila una baraja. Al revés parece que se caen.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Producto</p>
      <h4 class="mt-2 text-base font-semibold">Tres</h4>
      <p class="mt-2 text-sm text-text-secondary">Solo la celda que pasa por el centro está de frente: la meseta es un punto, no un tramo.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Datos</p>
      <h4 class="mt-2 text-base font-semibold">Cuatro</h4>
      <p class="mt-2 text-sm text-text-secondary">Sin JavaScript la tira se arrastra igual. Lo que faltan son los dos botones.</p>
    </article>
  </div>
  <div class="car-celda">
    <article class="rounded-xl border border-border bg-bg-surface-1 p-5">
      <p class="font-mono text-xs uppercase tracking-[0.16em] text-text-muted">Interno</p>
      <h4 class="mt-2 text-base font-semibold">Cinco</h4>
      <p class="mt-2 text-sm text-text-secondary">Al llegar al final el botón no desaparece: se apaga.</p>
    </article>
  </div>
</div>

Plana, sin coverflow

El 3D es la capa de arriba, nunca el camino principal. data-coverflow="no" lo apaga: queda la tira que se arrastra y encaja, que ya funcionaba entera. Es también lo que se ve donde animation-timeline no existe —Firefox hoy—, así que conviene mirarlo.

Plana
Sin giro
Sigue encajando
Y sigue siendo scroll nativo
El marcado
<div class="car" data-coverflow="no" style="--car-ancho: 15rem" role="region" aria-label="Sin coverflow" tabindex="0">
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Plana</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Sin giro</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Sigue encajando</article></div>
  <div class="car-celda"><article class="rounded-xl border border-border bg-bg-surface-1 p-4 text-sm">Y sigue siendo scroll nativo</article></div>
</div>

Cuándo NO usarla

Es la parte que más valor tiene de esta página. Cada línea está aquí porque costó algo: se probó, se retiró, o se perdieron horas.

Clases

.car
La tira. Scroll nativo con scroll-snap-type: x mandatory y tabindex="0", así que recibe foco de teclado — y por eso tiene :focus-visible azul. El foco es azul, nunca naranja: aquí el naranja significa punto de decisión y el azul interactividad.
.car-celda
La celda fija el ancho y encaja. El hijo de dentro es el único que se transforma. No metas ninguna transform aquí.
.car-mandos / .car-boton
Los dos botones. Van fuera de la tira, arriba junto al titular. La flecha se adelanta un poco en la dirección que va a mover la tira: el botón dice hacia dónde antes de pulsarlo.
[data-coverflow="no"]
Apaga el 3D y deja la tira plana.

Mandos

Custom properties que se redefinen desde fuera sin tocar el archivo del sistema.

--car-sangria 1rem / 1.5rem / 2rem
Cuánto se sale la tira del margen del contenedor. Es una variable local para que el margen negativo, el relleno y el scroll-padding no puedan desincronizarse: los tres leen el mismo sitio.
--car-ancho min(20rem, 82vw) / 21rem / 24rem
El ancho de una celda. En móvil llena casi la pantalla pero deja asomar la siguiente, que es lo que dice que la tira se arrastra.

Tokens de los que depende

Sin tokens.css importado antes, estas variables no existen: la pieza no falla, se pinta rota en silencio.

  • --color-border
  • --color-border-hover
  • --color-link
  • --color-text-primary
  • --color-bg-surface-1
  • --radius-xl
  • --radius-full
  • --dur-fast
  • --dur-base
  • --dur-slow
  • --ease-spring
  • --ease-out-expo

Qué queda cuando algo falla

  • Sin JavaScript la tira se arrastra igual con dedo, trackpad o teclado: tiene tabindex y scroll nativo. Se pierden los dos botones, no el contenido.
  • Sin animation-timeline —Firefox hoy— queda el carrusel plano, que ya funcionaba entero. El @supports va delante a propósito y ese orden no se invierte.
  • Por debajo de 640px no hay coverflow: se ve una celda, y girarla sería girar la única cosa que hay que leer.
  • Con prefers-reduced-motion el coverflow no se activa, el scroll deja de animarse y los mandos ya no escalan. El hover y el foco siguen cambiando borde y fondo.

Accesibilidad

  • role="region" + aria-label + tabindex="0": la tira es un contenedor con scroll y tiene que poder recorrerse con teclado.
  • :focus-visible con outline: 2px solid var(--color-link). Azul, porque el naranja está reservado a punto de decisión.
  • Los mandos miden 2.75rem = 44px. El icono mide 1.05rem; el resto es blanco que existe para poder acertarle con el pulgar.
  • Los botones son iconos sin texto: aria-label obligatorio.

Lo que el sistema todavía no resuelve

Valores literales y huecos conocidos. Se escriben para que nadie los confunda con una decisión.

  • La opacidad 0.25 del mando apagado es literal: no hay escala de opacidades para estados inertes.
  • El perspective: 1600px también. No hay tokens de profundidad.