Ir al contenido
Evaluar mi proyecto
Componentes · FootersDisponible

Footer con marca gigante que sube

Barra de enlaces discreta y la marca en letras enormes que suben una a una al llegar al final de la página.

© 2026
Demostración interactiva

Código completo

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

footer-cortina.tsx71 líneas
Descargar
"use client";

import { useEffect, useRef, useState, type CSSProperties } from "react";

/** Footer editorial con la marca en letras gigantes que suben una a una al llegar al final de la página, y una barra de enlaces discreta. React + TypeScript, sin dependencias. */
export type FooterCortinaProps = {
  word?: string;
  links?: { label: string; href?: string }[];
  email?: string;
  legal?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcfco-root { box-sizing: border-box; container-type: inline-size; position: relative; width: 100%; overflow: hidden; padding: clamp(28px, 5cqi, 64px) clamp(16px, 4cqi, 56px) 0; background: #111; color: #f1ede4; font: 500 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcfco-root *, .gcfco-root *::before, .gcfco-root *::after { box-sizing: border-box; }
.gcfco-root :focus-visible { outline: 2px solid #ff6b3d; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .gcfco-root *, .gcfco-root *::before, .gcfco-root *::after { transition: none !important; transform: none !important; } }
.gcfco-bar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 18px 36px; padding-bottom: 28px; border-bottom: 1px solid #333; }
.gcfco-mail { color: inherit; font: 500 clamp(20px, 3cqi, 30px)/1.1 "Newsreader", Georgia, ui-serif, serif; text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 2px; }
.gcfco-mail:hover { color: #ff6b3d; }
.gcfco-links { display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 0; padding: 0; list-style: none; align-items: center; }
.gcfco-links a { color: #bdb7a8; text-decoration: none; }
.gcfco-links a:hover { color: #fff; }
.gcfco-word { display: flex; justify-content: center; margin: 0; padding: .05em 0 0; overflow: hidden; font: 900 clamp(64px, 24cqi, 360px)/.82 "Arial Black", "Impact", ui-sans-serif, system-ui, sans-serif; letter-spacing: -.05em; text-transform: uppercase; color: #f1ede4; user-select: none; }
.gcfco-l { display: inline-block; transform: translateY(105%); transition: transform 1s cubic-bezier(.16, 1, .3, 1); transition-delay: calc(var(--i) * 70ms); }
.gcfco-root[data-in="true"] .gcfco-l { transform: none; }
.gcfco-l:hover { color: #ff6b3d; }
.gcfco-legal { position: absolute; right: clamp(16px, 4cqi, 56px); bottom: 10px; font-size: 12px; color: #6f6a5f; mix-blend-mode: normal; }
@media (scripting: none) { .gcfco-l { transform: none; } }`;

export function FooterCortina({ word = "Hasta pronto", links = [{ label: "Proyectos" }, { label: "Servicios" }, { label: "Contacto" }, { label: "Aviso de privacidad" }], email = "hola@ejemplo.com", legal = "© 2026", className = "", style }: FooterCortinaProps) {
  const ref = useRef<HTMLElement>(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.25 });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return (
    <footer ref={ref} className={`gcfco-root ${className}`} style={style} data-in={seen}>
      <style>{CSS}</style>
      <div className="gcfco-bar">
        <a className="gcfco-mail" href={`mailto:${email}`}>
          {email}
        </a>
        <ul className="gcfco-links">
          {links.map((link) => (
            <li key={link.label}>
              <a href={link.href ?? "#"} onClick={(event) => !link.href && event.preventDefault()}>
                {link.label}
              </a>
            </li>
          ))}
        </ul>
      </div>
      <p className="gcfco-word" aria-label={word}>
        {Array.from(word).map((char, index) => (
          <span key={index} className="gcfco-l" aria-hidden="true" style={{ "--i": index } as CSSProperties}>
            {char === " " ? " " : char}
          </span>
        ))}
      </p>
      <span className="gcfco-legal">{legal}</span>
    </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-cortina

Ejemplo de uso de Footer con marca gigante que sube

Después de copiar footer-cortina.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { FooterCortina } from "./footer-cortina";

export default function Ejemplo() {
  return <FooterCortina word="Ejemplo" />;
}

Propiedades de FooterCortina

FooterCortina acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

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

Accesibilidad y rendimiento

El archivo tiene 71 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Footer con marca gigante que sube 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-*.
  • 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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)