# Footer (Lumen Halo): prompt.md (v1.3.0)

- id: `glass-footer` · version 1.3.0 · component · pro (All-Access)
- category: Navigation
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add @beautiful-ui-pro/glass-footer`
- npm dependencies: motion@^13, @web-kits/audio@^0.2
- registry dependencies: https://beautiful-ui.dev/r/beautiful-ui.json
- docs: https://beautiful-ui.dev/components/glass-footer
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

A liquid-glass site footer in dark and light: link columns with a gliding lens, a newsletter pill with animated states, socials, back to top and a giant wordmark lit by the cursor.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `motion`, `@web-kits/audio`.
- Source: `components/beautiful-ui/glass-footer.tsx`, shared code in `lib/beautiful-ui/`.
- Exports to keep: `GlassFooter`, and every exported type.
- CSS: none to add. Themes, tokens and keyframes are inlined by the component (a deduped `<style>` built with `lumenThemeCss` from `lib/beautiful-ui/glass.tsx`); retheme through the `--glass-*` variables, never with Tailwind colour classes inside the component.

```tsx
import { GlassFooter } from "@/components/beautiful-ui/glass-footer";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `GlassFooter` | — | The site footer: link columns with a gliding lens, the newsletter field and the bottom bar. |

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `GlassSoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`, import from `@/components/beautiful-ui/glass-sound`); `GlassSoundToggle` is its mute control. Without a provider the audio engine never loads; `sound={false}` silences one instance.
- This item: links and buttons tap; success when the subscription lands; error on an invalid email or a rejected subscribe (and each re-shake); swoosh on back to top; needs a GlassSoundProvider; sound={false} silences this instance.

## Match the original
- Read `components/beautiful-ui/glass-footer.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- site footer, marketing footer, newsletter signup, link columns, social links, back to top
- The footer of a marketing site or product landing page, next to Glass Nav
- When the footer should collect newsletter signups with clear invalid, sending and done states
- Sites that want a memorable sign-off: the giant wordmark lights up under the cursor

### Not when
- Dense sitemaps with more than about five columns: use a plain link list
- Inside an app shell: use Glass Sidebar for app navigation

## Mistakes
- Wire onSubscribe to your real endpoint; without it the done state is only a demo (nothing is sent; development logs a warning)
- Pass linkComponent={Link} in Next.js (or your router's link); plain <a> links do full page reloads
- Inside a scroll container or modal, pass scrollTarget: back to top scrolls the window by default
- The wordmark and the link grid size from the footer's own width (a CSS size container on the root), so the footer needs a width from its parent: it is width:100%, but inside a shrink-to-fit parent (inline-block, width:fit-content, an auto-width flex item) it collapses; give that parent a width
- If you control status, update it from onStatusChange, or the form stays in the state you pass (submitting calls onStatusChange("sending") and then "done" or "error")
- Custom social names need an icon: only X, GitHub, LinkedIn and YouTube have built-in icons (glassFooterSocialIcons); others show an arrow
- copyright is a static string (SSR-safe). Compute the year on the server rather than in a client effect
- The root is the <footer> landmark: don't wrap it in another <footer>
- Throw an Error with a human message from onSubscribe to show it under the field
- The card clips its content (overflow hidden): don't place popovers inside it
- Keep column titles short and uppercase-style: they are mono labels, not headings

## Usage

```tsx
import Link from "next/link";
import { GlassFooter } from "@/components/beautiful-ui/glass-footer";

// Works as is with the designed content:
<GlassFooter />

// Your site (a server component can pass the year):
<GlassFooter
  brand={{ name: "Kitelabs", href: "/" }}
  tagline="The data platform for teams that ship."
  linkComponent={Link}
  columns={[
    { title: "PRODUCT", links: [{ label: "Pricing", href: "/pricing" }, { label: "Changelog", href: "/changelog", tag: "NEW", tagKind: "new" }] },
    { title: "RESOURCES", links: [{ label: "Docs", href: "https://docs.acme.com", external: true }] },
  ]}
  socials={[{ name: "X", href: "https://x.com/acme" }, { name: "GitHub", href: "https://github.com/acme" }]}
  legal={[{ label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }]}
  copyright={`© ${new Date().getFullYear()} Kitelabs, Inc.`}
  onSubscribe={async (email) => {
    const res = await fetch("/api/newsletter", { method: "POST", body: JSON.stringify({ email }) });
    if (!res.ok) throw new Error("Couldn't subscribe. Try again.");
  }}
  onLinkClick={(link) => analytics.track("footer_click", link)}
  labels={{ nav: "Site", backToTop: "Nach oben" }}
  style={{ ["--glass-max-width" as string]: "1200px" }}
/>

// Inside a scrolling panel instead of the window:
const panel = useRef<HTMLDivElement>(null);
<div ref={panel} className="h-full overflow-auto">… <GlassFooter scrollTarget={panel} /></div>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `sound` | `boolean \| "subtle"` | `true` | true plays Lumen cues when a GlassSoundProvider enables sound; false silences this instance; "subtle" plays at 55%. Without a provider nothing plays and the engine never loads. |
| `brand` | `{ name: string; href: string; mark?: ReactNode }` | `{ name: "Lumen", href: "/" }` | Logo and name, top left. The default mark is the eclipse. |
| `tagline` | `string` | `the designed line` | One or two sentences under the logo. |
| `columns` | `{ title: string; links: { label, href, tag?, tagKind?: "new" \| "count", external? }[] }[]` | `Product, Company, Resources` | Link columns. External links show ↗ and open in a new tab; tags add a NEW or count chip. |
| `socials` | `{ name: string; href: string; icon?: ReactNode }[]` | `X, GitHub, LinkedIn, YouTube` | Round icon links in a glass well. Built-in icons for X, GitHub, LinkedIn and YouTube; pass icon for others. |
| `legal` | `GlassFooterLegalLink[] ({ label: string; href: string })` | `Privacy, Terms, Security` | Small links next to the copyright. |
| `copyright` | `string` | `"© 2026 Lumen Labs, Inc."` | The copyright line. |
| `newsletter` | `{ label?, placeholder?, cta?, note?, invalidNote?, doneTitle?, doneDetail?, doneNote? } \| null` | `the designed copy` | Newsletter copy. null hides the newsletter. |
| `onSubscribe` | `(email: string) => Promise<void> \| void` |  | Called with a valid, trimmed email. Resolve for the done state; reject to show the error (the error's message, if it has one, else labels.subscribeError). Without it, the designed demo timing plays and nothing is sent (a one-time console warning in development). |
| `email / defaultEmail / onEmailChange` | `string / string / (email: string) => void` | `"" (uncontrolled)` | The email field, controlled or uncontrolled. onEmailChange fires on typing and when resetAfterSubscribe empties the field. |
| `status / onStatusChange` | `"idle" \| "error" \| "sending" \| "done" / (status) => void` |  | The newsletter state (GlassFooterStatus), controlled or uncontrolled. Pass status="done" for users who are already subscribed; onStatusChange reports every transition (invalid → error, submit → sending → done or error). |
| `validateEmail` | `(email: string) => boolean \| string` | `isGlassFooterEmail` | Replaces the built-in check (name@domain.tld). Return true for valid, false to show newsletter.invalidNote, or a string to show that message. |
| `resetAfterSubscribe` | `number` |  | Milliseconds the done state shows before the form empties again for another address (kiosks, demo pages). Default: it stays done. |
| `wordmark` | `string \| null` | `brand.name` | The giant word across the bottom. null hides it. |
| `backToTop` | `boolean` | `true` | Round button that smooth-scrolls to the top. |
| `scrollTarget` | `RefObject<HTMLElement \| null>` | `the window` | What back to top scrolls: pass a ref to your scroll container or modal body when the page doesn't scroll the window. |
| `onBackToTop` | `(event: MouseEvent) => void` |  | Called when back to top is pressed. Call event.preventDefault() to skip the built-in scroll (e.g. to scroll with your own library). |
| `linkComponent` | `React.ElementType` | `"a"` | Renders every link (brand, columns, legal, socials) with href, className, style, children, onClick, onMouseEnter, onFocus, onBlur, aria-label and, for external links, target/rel. Pass Next.js Link for client-side navigation. |
| `onLinkClick` | `(link: { kind: "brand" \| "column" \| "legal" \| "social"; label; href; external }, event) => void` |  | Fires on any footer link click, for analytics. preventDefault() in it stops the navigation. |
| `labels` | `Partial<GlassFooterLabels>` | `defaultGlassFooterLabels` | Every built-in string that isn't content: nav ("Footer"), email ("Email address"), subscribing, subscribed, subscribeError, opensInNewTab, social: (name) => aria-label (opensInNewTab is appended), backToTop. Newsletter copy stays in newsletter. |
| `maxWidth` | `number \| string` | `1040 (var(--glass-max-width, 1040px))` | Largest width of the card; it fills its container up to this. Also settable with the CSS variable --glass-max-width on the footer or any ancestor. "none" removes the cap. |
| `motion` | `"full" \| "subtle" \| "off"` | `"full"` | full: the designed motion. subtle: calm curve, shorter. off: instant. prefers-reduced-motion is always respected. |
| `theme` | `"system" \| "dark" \| "light"` | `"system"` | system follows a .dark / .light class or data-theme on an ancestor (next-themes, shadcn), else the OS. |
| `className / style / ref / id / aria-* / data-* / on*` | `HTML attributes of <footer>` |  | Passed to the root <footer> (the contentinfo landmark). style is applied last, so it can override maxWidth or set CSS variables inline; ref points at the <footer>. |
| `CSS variables` | `--glass-max-width, --glass-bad, --glass-good (+ -dark / -light, -text)` |  | Set on the footer or any ancestor. --glass-max-width caps the card (default 1040px). --glass-bad colours the invalid ring and error note, --glass-good the done ring and note; -dark / -light variants win per theme and --glass-bad-text / --glass-good-text set just the note text. Defaults are the designed colours. |

Full docs: https://beautiful-ui.dev/components/glass-footer

## Customising
- Theme: `theme="system"` (default) follows a `.dark` / `.light` class or `data-theme` on an ancestor, else the OS. `"dark"` / `"light"` pin one.
- Motion: `motion="full"` (default) | `"subtle"` | `"off"`. prefers-reduced-motion is always respected.
- Colours: the family variables (`--glass-accent`, `--glass-good`, `--glass-series-1` …) on the component or any ancestor; the neutral glass is `--lg-*` in `lib/beautiful-ui/glass.tsx`.
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| Tab | Move through links; the lens follows focus |
| Enter in the email field | Subscribe (invalid emails shake and explain) |
| Enter on back to top | Scroll to the top of the page |

## Motion inventory

| Interaction | What moves |
|---|---|
| Sound | links and buttons tap; success when the subscription lands; error on an invalid email or a rejected subscribe (and each re-shake); swoosh on back to top; needs a GlassSoundProvider; sound={false} silences this instance |
| Hover or focus a link | One glass lens glides across all columns (550ms); the text nudges 2px and an arrow slides in |
| Hover a social icon | A glass disc slides to it (450ms); the icon scales 1.12 with a slight overshoot |
| Subscribe | Invalid: the pill shakes with a red ring. Sending: the arrow slides out, a spinner turns. Done: the label collapses, a check pops in, the confirmation slides up |
| First seen | The wordmark rises into place (1.2s) when the footer first scrolls into view, not on mount; motion="off" shows it at rest |
| motion="subtle" / "off" / reduced motion | Subtle: calm curve, shorter. Off: instant. Reduced: 150ms fades only |

## Accessibility contract (preserve when editing)
- A real <footer> with a labelled <nav> for the link columns; the lens follows keyboard focus as well as the pointer
- The email field has a label and aria-describedby pointing at the helper line, which becomes an alert on errors and a status when done
- Invalid emails keep focus in the field; typing clears the error
- Social links and back to top have labels; external links and socials announce that they open a new tab
- Each link column is a labelled group (its mono title); the email field is name="email" autocomplete="email" and read-only while sending
- The wordmark is decorative (aria-hidden); prefers-reduced-motion removes the shake, spinner, overshoot and spotlight

## Install

```bash
npx shadcn@latest add @beautiful-ui-pro/glass-footer
```

Pro item: needs the `@beautiful-ui-pro` registry in `components.json` and `BEAUTIFUL_UI_TOKEN` in `.env.local` (https://beautiful-ui.dev/account). Setup: https://beautiful-ui.dev/docs/pro

## Credits
- Sound by @web-kits/audio (https://www.npmjs.com/package/@web-kits/audio)
