Hero de obturador luminoso
Hero con seis hojas luminosas que abren su máscara según el cursor; composición estática completa para móvil y movimiento reducido.
El siguiente capítulo empieza contigo.
Dale forma a una idea que merezca ser vista.
Abrir posibilidadesMueve el cursor para abrir la luz · composición de muestra
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useRef, type CSSProperties, type PointerEvent } from "react";
/** Hero de obturador luminoso. React + TypeScript, sin dependencias. Personaliza con variables --gcys-*. */
export type HeroObturadorProps = {
title?: string;
description?: string;
href?: string;
label?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcys-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gcys-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gcys-root *, .gcys-root *::before, .gcys-root *::after { box-sizing: border-box; }
.gcys-root button, .gcys-root input, .gcys-root textarea, .gcys-root select { font: inherit; }
.gcys-root button, .gcys-root a { -webkit-tap-highlight-color: transparent; }
.gcys-root :focus-visible { outline: 2px solid var(--gcys-accent, #25724b); outline-offset: 4px; }
.gcys-root ::selection { background: var(--gcys-accent, #25724b); color: #fff; }
.gcys-root button:disabled { opacity: .5; cursor: not-allowed; }
.gcys-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gcys-root *, .gcys-root *::before, .gcys-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gcys-root { position:relative; isolation:isolate; width:100%; min-height:590px; overflow:hidden; background:var(--gcys-bg,#151312); color:var(--gcys-fg,#faf0e3); padding:64px clamp(24px,6vw,76px) 28px; }
.gcys-light { position:absolute; inset:0 0 0 47%; z-index:-1; display:flex; transform:skewX(-15deg); opacity:.8; }
.gcys-light i { flex:1; background:linear-gradient(180deg,#c87036,#592d19 65%,#151312); border-left:1px solid #ebaa6e40; clip-path:inset(0 calc(100% - var(--gcys-open,48%)) 0 0); transition:clip-path .65s cubic-bezier(.16,1,.3,1); }
.gcys-light i:nth-child(even) { transform:translateY(-70px); }
.gcys-content { max-width:620px; }
.gcys-content h2 { font-size:clamp(42px,5.5vw,76px); font-weight:600; line-height:1.06; letter-spacing:-.035em; text-wrap:balance; margin:0 0 28px; }
.gcys-content p { font-size:16px; color:#d2bda9; max-width:30ch; margin:0 0 34px; }
.gcys-content a { display:inline-flex; align-items:center; gap:30px; padding:14px 22px; min-height:48px; border:1px solid #b8a087; border-radius:3px; color:#faf0e3; font-size:13px; font-weight:600; text-decoration:none; transition:background .2s; }
.gcys-content a:hover { background:#38251a; }
.gcys-caption { font-size:11px; color:#c3ad97; margin:70px 0 0; max-width:40ch; }
@media(max-width:540px) { .gcys-light { inset:0 0 0 20%; opacity:.4; }
.gcys-content h2 { font-size:44px; } }`;
export function HeroObturador({
title = "El siguiente capítulo empieza contigo.",
description = "Dale forma a una idea que merezca ser vista.",
href = "#empezar",
label = "Abrir posibilidades",
className = "",
style,
}: HeroObturadorProps) {
const root = useRef<HTMLDivElement>(null);
function move(e: PointerEvent<HTMLDivElement>) {
if (
e.pointerType !== "mouse" ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
return;
const box = e.currentTarget.getBoundingClientRect();
root.current?.style.setProperty(
"--gcys-open",
`${20 + ((e.clientX - box.left) / box.width) * 65}%`,
);
}
return (
<div
ref={root}
className={`gcys-root ${className}`}
style={style}
onPointerMove={move}
onPointerLeave={() => root.current?.style.removeProperty("--gcys-open")}
>
<style>{CSS}</style>
<div className="gcys-light" aria-hidden="true">
<i />
<i />
<i />
<i />
<i />
<i />
</div>
<section className="gcys-content">
<h2>{title}</h2>
<p>{description}</p>
<a href={href}>
{label}
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<path d="M5 12h14m-6-6 6 6-6 6" />
</svg>
</a>
</section>
<p className="gcys-caption">
Mueve el cursor para abrir la luz · composición de muestra
</p>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-obturadorEjemplo de uso de Hero de obturador luminoso
Después de copiar hero-obturador.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { HeroObturador } from "./hero-obturador";
export default function Ejemplo() {
return <HeroObturador title="Ejemplo" />;
}Propiedades de HeroObturador
HeroObturador acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
href? | string | Opcional. |
label? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Hero de obturador luminoso 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 |
|---|---|
--gcys-fg | #202421 |
--gcys-accent | #25724b |
--gcys-bg | #151312 |
--gcys-open | 48% |
Accesibilidad y rendimiento
El archivo tiene 100 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Hero de obturador luminoso 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-*.
- Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
- Es un componente de cliente de Next.js: incluye la directiva "use client".