Equipo con tarjetas que se voltean
Tarjetas de iniciales que giran al apuntar o enfocar y muestran una frase corta. Sin fotos externas; personas de muestra.
Un equipo pequeño y senior
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Sección de equipo con tarjetas de iniciales: al apuntar o enfocar, la tarjeta se voltea y muestra una frase corta. Sin fotos externas. Personas de muestra. React + TypeScript, sin dependencias. */
export type SeccionEquipoPersona = { name: string; role: string; bio: string; hue: number };
export type SeccionEquipoProps = {
title?: string;
people?: SeccionEquipoPersona[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcseq-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(32px, 6cqi, 80px) clamp(18px, 5cqi, 72px); background: #fff; color: #18181b; font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcseq-root *, .gcseq-root *::before, .gcseq-root *::after { box-sizing: border-box; }
.gcseq-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcseq-root :focus-visible { outline: 3px solid #2563eb; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcseq-in { transition-duration: .01ms !important; } }
.gcseq-title { margin: 0 0 32px; font: 700 clamp(28px, 4.4cqi, 48px)/1.05 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.04em; }
.gcseq-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 18px; margin: 0; padding: 0; list-style: none; }
.gcseq-card { position: relative; display: block; width: 100%; aspect-ratio: 4 / 5; padding: 0; border: 0; background: none; color: inherit; text-align: left; cursor: pointer; perspective: 900px; }
.gcseq-in { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
.gcseq-card:hover .gcseq-in, .gcseq-card:focus-visible .gcseq-in, .gcseq-card[aria-pressed="true"] .gcseq-in { transform: rotateY(180deg); }
.gcseq-face { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end; padding: 18px; border-radius: 20px; backface-visibility: hidden; -webkit-backface-visibility: hidden; overflow: hidden; }
.gcseq-front { background: hsl(var(--h) 80% 90%); }
.gcseq-front .gcseq-ini { position: absolute; top: 14px; left: 18px; font: 900 clamp(64px, 11cqi, 120px)/.9 "Arial Black", ui-sans-serif, sans-serif; letter-spacing: -.06em; color: hsl(var(--h) 70% 48%); }
.gcseq-back { background: hsl(var(--h) 60% 22%); color: #fff; transform: rotateY(180deg); justify-content: center; }
.gcseq-name { font-weight: 700; font-size: 17px; }
.gcseq-role { color: #52525b; font-size: 14px; }
.gcseq-back .gcseq-role { color: hsl(var(--h) 80% 82%); margin-bottom: 10px; }
.gcseq-back p { margin: 0; font: 400 17px/1.35 "Newsreader", Georgia, ui-serif, serif; }`;
const DEFAULT: SeccionEquipoPersona[] = [
{ name: "Persona Uno", role: "Dirección técnica", bio: "Prefiere un sistema aburrido y confiable a uno vistoso que falla.", hue: 220 },
{ name: "Persona Dos", role: "Diseño de producto", bio: "Dibuja primero en papel y solo después en pantalla.", hue: 14 },
{ name: "Persona Tres", role: "Desarrollo", bio: "Cree que el mejor código es el que no hubo que escribir.", hue: 150 },
{ name: "Persona Cuatro", role: "Calidad y pruebas", bio: "Encuentra el error que tú jurabas que no existía.", hue: 280 },
];
export function SeccionEquipo({ title = "Un equipo pequeño y senior", people = DEFAULT, className = "", style }: SeccionEquipoProps) {
const [pressed, setPressed] = useState<number | null>(null);
return (
<section className={`gcseq-root ${className}`} style={style}>
<style>{CSS}</style>
<h2 className="gcseq-title">{title}</h2>
<ul className="gcseq-grid">
{people.map((person, i) => (
<li key={person.name}>
<button className="gcseq-card" type="button" aria-pressed={pressed === i} aria-label={`${person.name}, ${person.role}. ${person.bio}`} style={{ "--h": person.hue } as CSSProperties} onClick={() => setPressed(pressed === i ? null : i)} onBlur={() => setPressed((value) => (value === i ? null : value))}>
<span className="gcseq-in" aria-hidden="true">
<span className="gcseq-face gcseq-front">
<span className="gcseq-ini">{person.name.split(" ").map((part) => part[0]).join("").slice(0, 2)}</span>
<span className="gcseq-name">{person.name}</span>
<span className="gcseq-role">{person.role}</span>
</span>
<span className="gcseq-face gcseq-back">
<span className="gcseq-role">{person.role}</span>
<p>“{person.bio}”</p>
</span>
</span>
</button>
</li>
))}
</ul>
</section>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-equipoEjemplo de uso de Equipo con tarjetas que se voltean
Después de copiar seccion-equipo.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { SeccionEquipo } from "./seccion-equipo";
export default function Ejemplo() {
return <SeccionEquipo title="Ejemplo" />;
}Propiedades de SeccionEquipo
SeccionEquipo acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
people? | SeccionEquipoPersona[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 70 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Equipo con tarjetas que se voltean por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".