Titular que sube de una máscara
Titular editorial que sube línea por línea desde una máscara al entrar en pantalla, con inclinación inicial y cursiva.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useRef, useState, type CSSProperties } from "react";
/** Titular que sube línea por línea desde una máscara al entrar en pantalla. Editorial, con una sola entrada y sin repetirse. React + TypeScript, sin dependencias. */
export type TextoCortinaProps = {
lines?: string[];
/** Milisegundos entre una línea y la siguiente. */
stagger?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gctct-root { display: block; color: var(--gctct-fg, #151515); font: 500 clamp(36px, 8.5vw, 92px)/1.02 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.035em; margin: 0; }
.gctct-root *, .gctct-root *::before, .gctct-root *::after { box-sizing: border-box; }
.gctct-line { display: block; overflow: hidden; padding: .06em 0 .12em; margin: -.06em 0 -.12em; }
.gctct-in { display: block; transform: translateY(108%) skewY(4deg); transform-origin: 0 100%; transition: transform 1.05s cubic-bezier(.16, 1, .3, 1); transition-delay: calc(var(--i) * var(--gap, 110ms)); }
.gctct-root[data-in="true"] .gctct-in { transform: none; }
.gctct-line:nth-child(2) .gctct-in { font-style: italic; }
@media (prefers-reduced-motion: reduce) { .gctct-in { transform: none; transition: none; } }
@media (scripting: none) { .gctct-in { transform: none; } }`;
export function TextoCortina({ lines = ["Software hecho", "a la medida de", "tu operación."], stagger = 110, className = "", style }: TextoCortinaProps) {
const ref = useRef<HTMLHeadingElement>(null);
const [seen, setSeen] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => entry.isIntersecting && (setSeen(true), io.disconnect()), { threshold: 0.3 });
io.observe(el);
return () => io.disconnect();
}, []);
return (
<h2 ref={ref} className={`gctct-root ${className}`} style={{ "--gap": `${stagger}ms`, ...style } as CSSProperties} data-in={seen} aria-label={lines.join(" ")}>
<style>{CSS}</style>
{lines.map((line, index) => (
<span key={line} className="gctct-line" aria-hidden="true">
<span className="gctct-in" style={{ "--i": index } as CSSProperties}>
{line}
</span>
</span>
))}
</h2>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/texto-cortinaEjemplo de uso de Titular que sube de una máscara
Después de copiar texto-cortina.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { TextoCortina } from "./texto-cortina";
export default function Ejemplo() {
return <TextoCortina />;
}Propiedades de TextoCortina
TextoCortina acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
lines? | string[] | Opcional. |
stagger? | number | Milisegundos entre una línea y la siguiente. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Titular que sube de una máscara con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gctct-fg | #151515 |
--gap | 110ms |
Accesibilidad y rendimiento
El archivo tiene 47 líneas (2 KB) y no necesita @keyframes. Esto es lo que hace Titular que sube de una máscara por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Se pausa cuando sale de la pantalla para no gastar batería ni CPU.
- Es un componente de cliente de Next.js: incluye la directiva "use client".