Footer bento
Baldosas de colores con marca, enlaces, estado y hora local en vivo.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Footer en bento: baldosas de colores suaves con la marca, los enlaces, un indicador de estado y la hora local en vivo.
* El reloj usa la zona horaria que le pases y se actualiza cada segundo.
*
* <BentoFooter brand="Estudio Lumen" timeZone="America/Mexico_City" city="Ciudad de México" status="Todos los sistemas funcionan" />
*/
import { useEffect, useState, type CSSProperties } from "react";
type Link = { label: string; href: string };
export type BentoFooterProps = {
brand?: string;
tagline?: string;
links?: Link[];
social?: Link[];
timeZone?: string;
city?: string;
status?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcfb-root { box-sizing: border-box; padding: clamp(14px, 3vw, 32px); background: #eef0f4; color: #1b2030; font-family: inherit; }
.gcfb-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: minmax(130px, auto); gap: 12px; }
.gcfb-tile { position: relative; display: flex; flex-direction: column; justify-content: space-between; gap: 18px; padding: 22px; overflow: hidden; border-radius: 22px; background: #fff; transition: transform .4s cubic-bezier(.16, 1, .3, 1); }
.gcfb-tile:hover { transform: translateY(-3px); }
.gcfb-tile h3 { margin: 0; font-size: 13px; font-weight: 600; opacity: .62; }
.gcfb-brand { grid-column: span 2; grid-row: span 2; background: #1b2030; color: #fff; }
.gcfb-brand strong { display: block; max-width: 11ch; font-size: clamp(32px, 4.4vw, 56px); font-weight: 800; line-height: .98; letter-spacing: -.045em; }
.gcfb-brand p { max-width: 30ch; margin: 0; color: #aab1c9; font-size: 15px; line-height: 1.5; }
.gcfb-links { background: #d8f0e4; } .gcfb-social { background: #ffe4cf; } .gcfb-time { background: #dfe5ff; } .gcfb-status { background: #fff3b8; }
.gcfb-tile ul { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.gcfb-tile a { color: inherit; font-size: 16px; font-weight: 600; text-decoration: none; }
.gcfb-tile a { text-decoration: underline; text-decoration-color: transparent; text-decoration-thickness: 2px; text-underline-offset: 5px; transition: text-decoration-color .2s; } .gcfb-tile a:hover { text-decoration-color: currentColor; }
.gcfb-tile a:focus-visible { outline: 2px solid #1b2030; outline-offset: 3px; border-radius: 3px; }
.gcfb-clock { font-size: clamp(30px, 3.4vw, 44px); font-weight: 800; letter-spacing: -.03em; font-variant-numeric: tabular-nums; line-height: 1; }
.gcfb-clock small { display: block; margin-top: 8px; font-size: 14px; font-weight: 600; letter-spacing: 0; opacity: .62; }
.gcfb-live { display: inline-flex; align-items: center; gap: 10px; font-size: 16px; font-weight: 700; line-height: 1.3; }
.gcfb-live i { position: relative; flex: none; width: 10px; height: 10px; border-radius: 50%; background: #16a34a; }
.gcfb-live i::after { content: ""; position: absolute; inset: 0; border-radius: 50%; background: inherit; animation: gcfb-ping 1.8s ease-out infinite; }
.gcfb-legal { grid-column: 1 / -1; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 20px; padding: 10px 6px 0; font-size: 13px; opacity: .65; }
@keyframes gcfb-ping { to { transform: scale(3.2); opacity: 0; } }
@media (max-width: 860px) { .gcfb-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .gcfb-brand { grid-row: auto; } }
@media (max-width: 520px) { .gcfb-grid { grid-template-columns: 1fr; } .gcfb-brand { grid-column: auto; } }
@media (prefers-reduced-motion: reduce) { .gcfb-live i::after { animation: none; } .gcfb-tile { transition: none; } .gcfb-tile:hover { transform: none; } }
`;
function useClock(timeZone: string) {
const [now, setNow] = useState<Date | null>(null);
useEffect(() => {
const tick = () => setNow(new Date());
const first = requestAnimationFrame(tick);
const timer = window.setInterval(tick, 1000);
return () => { cancelAnimationFrame(first); window.clearInterval(timer); };
}, []);
if (!now) return "--:--:--";
try { return new Intl.DateTimeFormat("es-MX", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, timeZone }).format(now); }
catch { return now.toLocaleTimeString("es-MX", { hour12: false }); }
}
export function BentoFooter({
brand = "Estudio Lumen",
tagline = "Diseñamos y construimos productos digitales con calma y detalle.",
links = [{ label: "Proyectos", href: "#proyectos" }, { label: "Servicios", href: "#servicios" }, { label: "Contacto", href: "#contacto" }],
social = [{ label: "Instagram", href: "#instagram" }, { label: "LinkedIn", href: "#linkedin" }, { label: "GitHub", href: "#github" }],
timeZone = "America/Mexico_City",
city = "Ciudad de México",
status = "Todos los sistemas funcionan",
className,
style,
}: BentoFooterProps) {
const time = useClock(timeZone);
return (
<>
<style>{CSS}</style>
<footer className={["gcfb-root", className].filter(Boolean).join(" ")} style={style}>
<div className="gcfb-grid">
<div className="gcfb-tile gcfb-brand"><strong>{brand}</strong><p>{tagline}</p></div>
<nav className="gcfb-tile gcfb-links" aria-label="Sitio"><h3>Explorar</h3><ul>{links.map((link) => <li key={link.href}><a href={link.href}>{link.label}</a></li>)}</ul></nav>
<nav className="gcfb-tile gcfb-social" aria-label="Redes"><h3>Síguenos</h3><ul>{social.map((link) => <li key={link.href}><a href={link.href}>{link.label}</a></li>)}</ul></nav>
<div className="gcfb-tile gcfb-time"><h3>Hora local</h3><div className="gcfb-clock" suppressHydrationWarning>{time}<small>{city}</small></div></div>
<div className="gcfb-tile gcfb-status"><h3>Estado</h3><span className="gcfb-live"><i aria-hidden="true" />{status}</span></div>
<div className="gcfb-legal"><span>© {new Date().getFullYear()} {brand}</span><span>Privacidad · Términos</span></div>
</div>
</footer>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/footer-bentoEjemplo de uso de Footer bento
Después de copiar bento-footer.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { BentoFooter } from "./bento-footer";
export default function Ejemplo() {
return <BentoFooter brand="Ejemplo" />;
}Propiedades de BentoFooter
BentoFooter acepta 9 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
brand? | string | Opcional. |
tagline? | string | Opcional. |
links? | Link[] | Opcional. |
social? | Link[] | Opcional. |
timeZone? | string | Opcional. |
city? | string | Opcional. |
status? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 94 líneas (6 KB) y define 1 animación con @keyframes. Esto es lo que hace Footer bento 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-*.
- Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
Un footer organizado como bento: baldosas de colores suaves con la marca, los enlaces, las redes, un indicador de estado con latido y un reloj con la hora local de la zona que elijas.
Incluye
Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Reloj en vivo con la zona horaria que pases (timeZone)
- Indicador de estado con animación de latido
- Baldosas que se reorganizan en pantallas estrechas
- Enlaces con subrayado al pasar el puntero
Cómo usarlo
Pasa brand, tagline, links, social, timeZone, city y status.