Back to Footers
Inspired by:Jolocky
Added on Oct 3, 2026
components/footers/dot-ripple/dot-ripple-footer.tsx
"use client"

import * as React from "react"

import { DotRippleCanvas } from "./dot-ripple-canvas"
import { footerStyles } from "./footer-styles"
import { DiscordIcon, GitHubIcon, XIcon, YouTubeIcon } from "./icons"
import {
  type DotRippleFooterProps,
  type LegalLinkItem,
  type NavLinkItem,
  type SocialLinkItem,
} from "./types"

const DEFAULT_SOCIAL_LINKS: SocialLinkItem[] = [
  { platform: "github", href: "#", label: "GitHub" },
  { platform: "discord", href: "#", label: "Discord" },
  { platform: "youtube", href: "#", label: "YouTube" },
  { platform: "x", href: "#", label: "X" },
]

const DEFAULT_NAV_LINKS: NavLinkItem[] = [
  { label: "Curriculum", href: "#" },
  { label: "Projects", href: "#" },
  { label: "Certifications", href: "#" },
  { label: "Community", href: "#" },
  { label: "Donate", href: "#" },
]

const DEFAULT_LEGAL_LINKS: LegalLinkItem[] = [
  { label: "Terms of Service", href: "#" },
  { label: "Privacy Policy", href: "#" },
]

export function DotRippleFooter({
  className = "",
  contactEmail = "hello@jolocky.dev",
  organizationName = "Jolocky Foundation",
  addressLines = ["2150 Folsom Street, Suite 4", "San Francisco, CA 94110"],
  socialLinks = DEFAULT_SOCIAL_LINKS,
  ctaText = "Join Jolocky",
  ctaBadgeText = "FREE",
  ctaHref = "#",
  navLinks = DEFAULT_NAV_LINKS,
  legalLinks = DEFAULT_LEGAL_LINKS,
  copyrightText = "© 2026 Jolocky. Free to learn, forever.",
  wordmarkText = "jolocky",
  withFadeOverlay = true,
}: DotRippleFooterProps) {
  const footerRef = React.useRef<HTMLElement | null>(null)
  const bgRef = React.useRef<HTMLDivElement | null>(null)
  const markRef = React.useRef<HTMLSpanElement | null>(null)

  // Scroll Parallax for Background Gradient & Giant Wordmark
  React.useEffect(() => {
    const footer = footerRef.current
    const bg = bgRef.current
    const mark = markRef.current
    if (!footer || !bg || !mark) return

    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
    if (reduceMotion) return

    const handleScroll = () => {
      const r = footer.getBoundingClientRect()
      const p = Math.max(0, Math.min(1, (window.innerHeight - r.top) / (r.height || 1)))
      bg.style.transform = `translate3d(0, ${(50 - p * 100).toFixed(1)}px, 0)`
      mark.style.transform = `translate3d(0, ${((1 - p) * 60).toFixed(1)}%, 0)`
    }

    window.addEventListener("scroll", handleScroll, { passive: true })
    handleScroll()
    return () => window.removeEventListener("scroll", handleScroll)
  }, [])

  const renderSocialIcon = (platform: "github" | "discord" | "youtube" | "x") => {
    switch (platform) {
      case "github":
        return <GitHubIcon size={28} />
      case "discord":
        return <DiscordIcon size={28} />
      case "youtube":
        return <YouTubeIcon size={28} />
      case "x":
        return <XIcon size={24} />
    }
  }

  return (
    <footer
      ref={footerRef}
      className={`jf${className ? ` ${className}` : ""}`}
    >
      <style>{footerStyles}</style>

      {/* Optional fade overlay transitioning previous section into footer */}
      {withFadeOverlay && <div className="jf__fade" aria-hidden="true" />}

      {/* Parallax Gradient Mesh Layer */}
      <div className="jf__bgwrap" aria-hidden="true">
        <div ref={bgRef} className="jf__bg" />
      </div>

      <div className="jf__inner">
        <div className="jf__main">
          {/* Contact Block */}
          <div className="jf__contact">
            <div className="jf__social">
              {socialLinks.map((item, idx) => (
                <a
                  key={idx}
                  href={item.href}
                  aria-label={item.label}
                  className={item.platform === "x" ? "is-x" : undefined}
                >
                  {renderSocialIcon(item.platform)}
                </a>
              ))}
            </div>

            {contactEmail && (
              <a className="jf__email" href={`mailto:${contactEmail}`}>
                {contactEmail}
              </a>
            )}

            {(organizationName || addressLines.length > 0) && (
              <address className="jf__address">
                {organizationName && (
                  <>
                    {organizationName},<br />
                  </>
                )}
                {addressLines.map((line, idx) => (
                  <React.Fragment key={idx}>
                    {line}
                    {idx < addressLines.length - 1 && <br />}
                  </React.Fragment>
                ))}
              </address>
            )}
          </div>

          {/* CTA Centerpiece with Procedural Dot Ripple Canvas */}
          <div className="jf__cta">
            <DotRippleCanvas className="jf__dots" />
            <div className="jf__cta-row">
              <span className="jf__spacer" />
              <span className="jf__bracket jf__bracket--l" />
              <span className="jf__line" />
              <a className="jf__btn" href={ctaHref}>
                {ctaText}
                {ctaBadgeText && <span className="jf__pill">{ctaBadgeText}</span>}
              </a>
              <span className="jf__line" />
              <span className="jf__bracket jf__bracket--r" />
              <span className="jf__spacer" />
            </div>
          </div>

          {/* Nav Column */}
          <nav className="jf__nav" aria-label="Footer navigation">
            {navLinks.map((link, idx) => (
              <a key={idx} href={link.href}>
                {link.label}
              </a>
            ))}
          </nav>
        </div>

        {/* Legal & Copyright */}
        <div className="jf__legal">
          {legalLinks[0] && (
            <a href={legalLinks[0].href} className="jf__terms">
              {legalLinks[0].label}
            </a>
          )}
          <span className="jf__copy">{copyrightText}</span>
          {legalLinks[1] && (
            <a href={legalLinks[1].href} className="jf__privacy">
              {legalLinks[1].label}
            </a>
          )}
        </div>
      </div>

      {/* Giant Parallax Wordmark */}
      <div className="jf__mark" aria-hidden="true">
        <span ref={markRef}>{wordmarkText}</span>
      </div>
    </footer>
  )
}
Dot Ripple Outro
dot-ripple
Jolocky Foundation,
2150 Folsom Street, Suite 4
San Francisco, CA 94110
Kinetic & Canvas Architecture
Built from scratch by Jolocky, with our own layout, color and motion.
Signature Standard
Precision interaction engineering with zero external runtime bloat.
Open Source Registry
Production-ready multi-file tree built for modern Next.js and Tailwind CSS.

Engineering Breakdown

Dot Ripple Outro — An architectural outro footer featuring a high-performance procedural canvas dot ripple, dual-axis scroll parallax, bracketed CTA centerpiece, and responsive adaptive dot fields.
Every interaction state, motion curve, and responsive edge-case has been extracted and reconstructed for seamless integration into modern React applications.
01
Zero — Primitive Dependency & Pure Standalone Pattern: 100% pure Canvas 2D + React without third—party UI primitive dependencies.
02
Procedural Canvas Ripple: Computes mathematical dot grids with concentric Gaussian wave propagation and random twinkling accents.
03
Adaptive Responsive Layouts: Automatically switches between dual flanking dot fields (desktop/tablet) and a single focused radial circle (mobile).
04
Offscreen Lifecycle Management: Pauses the requestAnimationFrame loop whenever the footer is scrolled outside the viewport for optimal performance.
05
Dual — Axis Parallax: Smoothly translates the vertical sunset gradient layer and container—query giant wordmark relative to scroll position.
Last Updated
Oct 3, 2026
Tags & Specifications
Canvas 2DProcedural RippleScroll ParallaxGeometric CTAZero Primitive
Color Palette
#16181d
#4f6fe0
#f3f4f8
#ffffff
Technology Stack
Engineered using established and reliable modern web technologies for peak runtime performance and developer ergonomics.
React
TypeScript
HTML Canvas
Scoped CSS
Inline SVG