Footer de señal reactiva
Footer oscuro con señal de barras que responde al enlace de contacto y al foco de teclado; ninguna animación automática.
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";
/** Footer de señal reactiva. React + TypeScript, sin dependencias. Personaliza con variables --gcyf-*. */
export type FooterSenalProps = {
brand?: string;
title?: string;
email?: string;
links?: { label: string; href: string }[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcyf-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gcyf-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gcyf-root *, .gcyf-root *::before, .gcyf-root *::after { box-sizing: border-box; }
.gcyf-root button, .gcyf-root input, .gcyf-root textarea, .gcyf-root select { font: inherit; }
.gcyf-root button, .gcyf-root a { -webkit-tap-highlight-color: transparent; }
.gcyf-root :focus-visible { outline: 2px solid var(--gcyf-accent, #25724b); outline-offset: 4px; }
.gcyf-root ::selection { background: var(--gcyf-accent, #25724b); color: #fff; }
.gcyf-root button:disabled { opacity: .5; cursor: not-allowed; }
.gcyf-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gcyf-root *, .gcyf-root *::before, .gcyf-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gcyf-root { width:100%; background:var(--gcyf-bg,#16191a); color:var(--gcyf-fg,#f2eadb); padding:40px clamp(24px,6vw,72px) 24px; }
.gcyf-signal { display:flex; height:110px; align-items:center; gap:5px; margin-bottom:35px; overflow:hidden; }
.gcyf-signal i { flex:1; min-width:2px; height:var(--gcyf-h); background:var(--gcyf-accent,#dda56b); transform:scaleY(.65); transition:transform .5s cubic-bezier(.16,1,.3,1); transition-delay:calc(var(--gcyf-i) * 7ms); }
.gcyf-root[data-hover="true"] .gcyf-signal i { transform:scaleY(1); }
.gcyf-root h2 { font-size:clamp(36px,5vw,64px); line-height:1.07; letter-spacing:-.035em; font-weight:600; max-width:17ch; margin:0 0 24px; text-wrap:balance; }
.gcyf-contact { display:inline-flex; align-items:center; gap:30px; color:#eac49f; font-size:16px; min-height:44px; text-underline-offset:5px; overflow-wrap:anywhere; }
.gcyf-bottom { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:24px; margin-top:64px; font-size:11px; color:#c7baaa; }
.gcyf-bottom strong { color:#f2eadb; font-size:28px; font-weight:600; letter-spacing:-.025em; }
.gcyf-bottom nav { display:flex; gap:20px; flex-wrap:wrap; }
.gcyf-bottom nav a { color:#c7baaa; text-decoration:none; min-height:44px; display:flex; align-items:center; }
.gcyf-bottom nav a:hover { color:#fff; }`;
export function FooterSenal({
brand = "SIGNAL",
title = "Hagamos que algo se mueva.",
email = "hola@example.com",
links = [
{ label: "Trabajo", href: "#trabajo" },
{ label: "Acerca de", href: "#acerca" },
{ label: "Privacidad", href: "#privacidad" },
],
className = "",
style,
}: FooterSenalProps) {
const [hover, setHover] = useState(false);
return (
<footer
className={`gcyf-root ${className}`}
style={style}
data-hover={hover}
>
<style>{CSS}</style>
<div className="gcyf-signal" aria-hidden="true">
{Array.from({ length: 32 }, (_, i) => (
<i
key={i}
style={
{
"--gcyf-h": `${18 + Math.round(Math.sin(i * 0.43) ** 2 * 80)}%`,
"--gcyf-i": i,
} as CSSProperties
}
/>
))}
</div>
<h2>{title}</h2>
<a
className="gcyf-contact"
href={`mailto:${email}`}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
onFocus={() => setHover(true)}
onBlur={() => setHover(false)}
>
{email}
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<path d="M5 19 19 5M5 5h14v14" />
</svg>
</a>
<div className="gcyf-bottom">
<strong>{brand}</strong>
<nav aria-label="Pie de página">
{links.map((link) => (
<a href={link.href} key={link.href}>
{link.label}
</a>
))}
</nav>
<span>Concepto de muestra</span>
</div>
</footer>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/footer-senalEjemplo de uso de Footer de señal reactiva
Después de copiar footer-senal.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { FooterSenal } from "./footer-senal";
export default function Ejemplo() {
return <FooterSenal brand="Ejemplo" />;
}Propiedades de FooterSenal
FooterSenal acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
brand? | string | Opcional. |
title? | string | Opcional. |
email? | string | Opcional. |
links? | { label: string; href: string }[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Footer de señal reactiva 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 |
|---|---|
--gcyf-fg | #202421 |
--gcyf-accent | #25724b |
--gcyf-bg | #16191a |
Accesibilidad y rendimiento
El archivo tiene 106 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Footer de señal reactiva 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".