Ir al contenido
Evaluar mi proyecto
Componentes · FootersDisponible

Footer en columnas con boletín

Marca, tres columnas de enlaces y suscripción por correo con estados.

Código completo

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

newsletter-footer.tsx115 líneas
Descargar
"use client";

/**
 * Footer clásico en columnas con formulario de boletín: marca y descripción, tres columnas de enlaces, suscripción por correo,
 * redes y fila legal. El formulario valida el correo y muestra su estado; conecta `onSubscribe` a tu servicio.
 *
 *   <NewsletterFooter
 *     brand="Estudio Lumen"
 *     columns={[{ title: "Producto", links: [{ label: "Funciones", href: "/funciones" }] }]}
 *     onSubscribe={async (email) => { await fetch("/api/boletin", { method: "POST", body: email }); }}
 *   />
 */

import { useState, type CSSProperties, type FormEvent } from "react";

export type FooterColumn = { title: string; links: { label: string; href: string }[] };

export type NewsletterFooterProps = {
  brand?: string;
  description?: string;
  columns?: FooterColumn[];
  social?: { label: string; href: string }[];
  /** Se llama con el correo válido. Si lanza un error, se muestra el mensaje de fallo. */
  onSubscribe?: (email: string) => void | Promise<void>;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcfc-root { box-sizing: border-box; padding: clamp(36px, 6vw, 72px) clamp(20px, 5vw, 64px) 28px; background: #12141b; color: #a7acbb; font-family: inherit; font-size: 15px; }
.gcfc-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) repeat(3, minmax(0, 1fr)); gap: clamp(28px, 4vw, 56px); }
.gcfc-brand { margin: 0; color: #fff; font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
.gcfc-desc { max-width: 34ch; margin: 12px 0 0; line-height: 1.6; }
.gcfc-form { display: flex; gap: 8px; max-width: 380px; margin-top: 24px; }
.gcfc-form input { flex: 1; min-width: 0; min-height: 44px; padding: 0 14px; border: 1px solid #2c303c; border-radius: 10px; outline: none; background: #1a1d27; color: #fff; font-family: inherit; font-size: 15px; transition: border-color .2s; }
.gcfc-form input:focus { border-color: #8b9dff; }
.gcfc-form input::placeholder { color: #727889; }
.gcfc-form button { min-height: 44px; padding: 0 18px; border: 0; border-radius: 10px; background: #fff; color: #12141b; font-family: inherit; font-size: 14px; font-weight: 700; cursor: pointer; transition: background-color .2s; }
.gcfc-form button:hover { background: #d9defa; }
.gcfc-form button:disabled { opacity: .6; cursor: progress; }
.gcfc-form input:focus-visible, .gcfc-form button:focus-visible { outline: 2px solid #8b9dff; outline-offset: 2px; }
.gcfc-status { min-height: 20px; margin: 10px 0 0; font-size: 13px; }
.gcfc-status[data-kind="ok"] { color: #7ee2a8; } .gcfc-status[data-kind="error"] { color: #ff9b9b; }
.gcfc-col h3 { margin: 0 0 16px; color: #fff; font-size: 14px; font-weight: 600; }
.gcfc-col ul { display: grid; gap: 11px; margin: 0; padding: 0; list-style: none; }
.gcfc-col a, .gcfc-legal a { color: inherit; text-decoration: none; transition: color .2s; }
.gcfc-col a:hover, .gcfc-legal a:hover { color: #fff; }
.gcfc-col a:focus-visible, .gcfc-legal a:focus-visible { outline: 2px solid #8b9dff; outline-offset: 3px; border-radius: 2px; }
.gcfc-legal { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 28px; margin-top: clamp(32px, 5vw, 56px); padding-top: 22px; border-top: 1px solid #262a36; font-size: 13px; }
.gcfc-social { display: flex; gap: 22px; margin: 0; padding: 0; list-style: none; }
@media (max-width: 860px) { .gcfc-grid { grid-template-columns: 1fr 1fr; } .gcfc-intro { grid-column: 1 / -1; } }
@media (max-width: 480px) { .gcfc-form { flex-direction: column; } .gcfc-grid { grid-template-columns: 1fr; } }
`;

const DEFAULT_COLUMNS: FooterColumn[] = [
  { title: "Producto", links: [{ label: "Funciones", href: "#funciones" }, { label: "Precios", href: "#precios" }, { label: "Novedades", href: "#novedades" }] },
  { title: "Empresa", links: [{ label: "Nosotros", href: "#nosotros" }, { label: "Blog", href: "#blog" }, { label: "Contacto", href: "#contacto" }] },
  { title: "Recursos", links: [{ label: "Documentación", href: "#docs" }, { label: "Guías", href: "#guias" }, { label: "Estado", href: "#estado" }] },
];

export function NewsletterFooter({
  brand = "Estudio Lumen",
  description = "Herramientas simples para equipos que construyen cosas. Una carta al mes, sin relleno.",
  columns = DEFAULT_COLUMNS,
  social = [{ label: "LinkedIn", href: "#linkedin" }, { label: "GitHub", href: "#github" }, { label: "YouTube", href: "#youtube" }],
  onSubscribe,
  className,
  style,
}: NewsletterFooterProps) {
  const [email, setEmail] = useState("");
  const [state, setState] = useState<{ kind: "idle" | "busy" | "ok" | "error"; message: string }>({ kind: "idle", message: "" });

  async function submit(event: FormEvent) {
    event.preventDefault();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setState({ kind: "error", message: "Escribe un correo válido." });
    setState({ kind: "busy", message: "" });
    try {
      await onSubscribe?.(email);
      setEmail("");
      setState({ kind: "ok", message: "Listo, revisa tu bandeja para confirmar." });
    } catch {
      setState({ kind: "error", message: "No pudimos suscribirte. Inténtalo de nuevo." });
    }
  }

  return (
    <>
      <style>{CSS}</style>
      <footer className={["gcfc-root", className].filter(Boolean).join(" ")} style={style}>
        <div className="gcfc-grid">
          <div className="gcfc-intro">
            <p className="gcfc-brand">{brand}</p>
            <p className="gcfc-desc">{description}</p>
            <form className="gcfc-form" onSubmit={submit} noValidate>
              <input type="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="tu@correo.com" aria-label="Correo para el boletín" autoComplete="email" />
              <button type="submit" disabled={state.kind === "busy"}>{state.kind === "busy" ? "Enviando…" : "Suscribirme"}</button>
            </form>
            <p className="gcfc-status" data-kind={state.kind} role="status">{state.message}</p>
          </div>
          {columns.map((column) => (
            <nav className="gcfc-col" key={column.title} aria-label={column.title}>
              <h3>{column.title}</h3>
              <ul>{column.links.map((link) => <li key={link.href}><a href={link.href}>{link.label}</a></li>)}</ul>
            </nav>
          ))}
        </div>
        <div className="gcfc-legal">
          <span>© {new Date().getFullYear()} {brand}. Todos los derechos reservados.</span>
          <ul className="gcfc-social">{social.map((item) => <li key={item.href}><a href={item.href}>{item.label}</a></li>)}</ul>
        </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-columnas

Ejemplo de uso de Footer en columnas con boletín

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

ejemplo.tsx5 líneas
import { NewsletterFooter } from "./newsletter-footer";

export default function Ejemplo() {
  return <NewsletterFooter brand="Ejemplo" />;
}

Propiedades de NewsletterFooter

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

PropiedadTipoDescripción
brand?stringOpcional.
description?stringOpcional.
columns?FooterColumn[]Opcional.
social?{ label: string; href: string }[]Opcional.
onSubscribe?(email: string) => void | Promise<void>Se llama con el correo válido. Si lanza un error, se muestra el mensaje de fallo.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 115 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Footer en columnas con boletín por quien lo usa:

  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

Acerca del producto

El footer clásico: marca y descripción, formulario de boletín con validación y estados (enviando, listo, error), tres columnas de enlaces, redes y fila legal. Conecta onSubscribe a tu servicio de correo.

Incluye

Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.

  • Validación del correo y mensajes de estado accesibles
  • onSubscribe asíncrono: si falla muestra el error
  • Columnas y redes configurables
  • Se reorganiza en dos columnas y una

Cómo usarlo

Pasa columns, social y onSubscribe. Si no pasas onSubscribe, el formulario solo valida y confirma.