Ir al contenido
Evaluar mi proyecto
Componentes · FootersDisponible

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.

Hagamos que algo se mueva.

hola@example.com
SIGNALConcepto de muestra

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

footer-senal.tsx106 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/footer-senal

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
brand?stringOpcional.
title?stringOpcional.
email?stringOpcional.
links?{ label: string; href: string }[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

VariableValor 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)