--- name: liquid-metal-border description: Add and tune animated liquid-metal WebGL borders with the React `metal-fx` package. Use when buttons, icon controls, chips, tabs, cards, or selected surfaces need a metallic active, selected, hover, focus, or premium border; when implementing the MetalFx component from metal.jakubantalik.com; or when troubleshooting its presets, themes, strength, glow, reflections, sizing, radius, animation, accessibility, SSR, or performance. --- # Liquid Metal Border ## Core Contract 1. Use `metal-fx` in React 18 or newer. 2. Wrap exactly one real host element such as a `button`, `a`, `div`, or `article`. 3. Keep the host semantic and interactive. `MetalFx` is visual framing, not the control. 4. Reserve the animated border for active, selected, focused, hovered, or primary surfaces. Do not put it around every control. 5. Keep a static CSS border or focus outline as the fallback. WebGL decoration must never carry essential state by itself. 6. Pause non-active instances and reduced-motion experiences. 7. Test light and dark modes independently. Reflections intentionally render only in dark mode. ## Inspect Before Editing - Confirm the project uses React and has `react` and `react-dom` version 18 or newer. - Find the real control, its dimensions, radius, background, border, outline, shadow, active-state source, and theme source. - Decide whether the metal should be always visible or driven by `active`, `selected`, hover, or focus state. - Check whether the project has a manual theme toggle. Use that state instead of `theme="auto"` when it does not follow the OS. - Identify nearby elements that genuinely benefit from reflected light. Do not add reflections by default. - Check the installed `metal-fx` version before relying on the prop surface below. ## Install and Import Install with the project's package manager: ```bash npm install metal-fx # pnpm add metal-fx # yarn add metal-fx # bun add metal-fx ``` Import the named React component: ```tsx import { MetalFx } from "metal-fx"; ``` Do not import a stylesheet. The package injects its component styles. It supports ESM and CommonJS builds, is SSR-safe, and mounts the WebGL pipeline after hydration. In a Next.js App Router project, render it from a Client Component because the component uses client-side React hooks. ## Baseline Button Start with a restrained border before adding reflections or custom shader geometry: ```tsx import { MetalFx } from "metal-fx"; export function UpgradeButton() { return ( ); } ``` The wrapper measures the child, paints the metal ring on top, and keeps the child interactive. Size the child normally with CSS, Tailwind, or inline styles. ## Active-State Pattern Keep one mounted instance and drive its intensity from real component state. Use `aria-pressed`, `aria-selected`, or the native selected-state mechanism so the visual effect is not the only signal. ```tsx "use client"; import { useEffect, useState } from "react"; import { MetalFx } from "metal-fx"; function useReducedMotion() { const [reduced, setReduced] = useState(false); useEffect(() => { const query = window.matchMedia("(prefers-reduced-motion: reduce)"); const sync = () => setReduced(query.matches); sync(); query.addEventListener("change", sync); return () => query.removeEventListener("change", sync); }, []); return reduced; } export function LiquidMetalToggle({ selected, onSelect, }: { selected: boolean; onSelect: () => void; }) { const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); const reducedMotion = useReducedMotion(); const highlighted = selected || hovered || focused; return ( ); } ``` ```css .liquid-metal-toggle { background: #18181b; border-radius: 999px; } .liquid-metal-toggle__control { min-height: 40px; padding: 0 18px; color: #fafafa; background: transparent; border: 0; border-radius: 999px; box-shadow: none; } .liquid-metal-toggle__control:focus-visible { outline: 2px solid #ffffff; outline-offset: 3px; } ``` Use `paused` to stop non-active canvas updates; it freezes the last frame without hiding the ring. `strength` changes rendered opacity from `0` to `1` but does not slow the shader. ## Static Border and Card Pattern Use the wrapper as the surface when the child must remain transparent: ```tsx

Realtime review

Keep feedback attached to the work.

``` Keep the article background transparent so it does not cover the ring. Use the child for content layout and the `MetalFx` wrapper for the visible surface. ## Complete Customization Surface | Prop | Values and default | Use | | --- | --- | --- | | `children` | One React host element; required | Preserve the real button, link, chip, card, or icon control. | | `variant` | `"button"` default, `"circle"` | Choose a 1 px pill-style baseline at shader scale `1.6`, or a 2 px compact-circle baseline at scale `1.3`. The measured child still controls the rendered size. | | `preset` | `"chromatic"` default, `"silver"`, `"gold"` | Choose iridescent rainbow, cool steel, or warm gold. Each includes dark and light tuning. | | `theme` | `"auto"` default, `"dark"`, `"light"` | Follow live `prefers-color-scheme` changes or pin the effect to the app theme. SSR starts dark, then resolves on the client. | | `strength` | Number `0..1`; default `1` | Scale canvas and glow opacity. Use lower values for idle states and stronger values for active states. | | `paused` | Boolean; default `false` | Freeze this instance on its current shader frame while keeping the silhouette visible. | | `borderRadius` | Number in CSS pixels; optional | Override the radius. When omitted, read the child's computed radius on resize. | | `normalizeHostStyles` | Boolean; default `true` | Remove the child's background, border, outline, and shadow so they do not fight the ring. Set `false` when preserving a custom focus outline or fallback border. | | `reflectionTargets` | Array of React element refs; optional | Cast a soft mirrored reflection onto selected neighbouring elements. Dark mode only. | | `disableGlow` | Boolean; default `false` | Remove the wandering halo while retaining the shader ring. | | `shaderScale` | Number; variant baseline by default | Increase to zoom into larger metal pattern features; decrease to zoom out. | | `ringCssPx` | Number; variant baseline by default | Override the visible ring thickness in CSS pixels. | | `scale` | Number; default `1` | Scale every absolute-pixel engine constant together for CSS zoom or deliberately enlarged UI systems. | | `className` | String; optional | Style the `MetalFx` wrapper, not the child. | | `style` | React CSS properties; optional | Size or style the wrapper surface directly. | | `ref` | React ref; optional | Access the forwarded wrapper `HTMLDivElement`. | Other valid `HTMLDivElement` attributes are forwarded to the wrapper. ## Tuning Defaults - Primary pill button: `variant="button"`, `strength={0.75}`, `ringCssPx={1}`. - Selected tab or filter: idle strength `0.1–0.2`, selected strength `0.75–0.9`. - Icon button: `variant="circle"`, explicit square child dimensions, `strength={0.7}`. - Large card: `ringCssPx={1}`, `shaderScale={1.3–1.6}`, `disableGlow`. - Premium CTA: start with `chromatic`; use `silver` for neutral UI and `gold` only for warm or luxury palettes. - Quiet active state: disable glow before reducing the ring below legibility. - Doubled design system: use `scale={2}` instead of independently doubling shader, ring, glow, and reflection values. Keep `strength <= 0.9` for routine controls. Full-strength chromatic metal can dominate labels and icons. ## Sizing and Radius Prefer sizing the child: ```tsx ``` To make the frame larger than the child, size the wrapper and stretch the child: ```tsx ``` The wrapper uses `display: inline-flex`. Do not create cyclic percentage sizing where neither wrapper nor child has an intrinsic size. ## Proximity Reflections Pass only explicit refs to neighbouring elements: ```tsx import { useRef } from "react"; const chipRef = useRef(null); <> ``` Omit `reflectionTargets` to disable reflection work. In light mode, reflections are skipped automatically. ## Accessibility and Motion - Preserve native `button`, `a`, and form semantics inside the wrapper. - Keep active state in `aria-pressed`, `aria-selected`, checked state, or current-route state. - Do not rely on hue or animation alone. Retain text, icon, position, weight, or a static edge change. - Be careful with `normalizeHostStyles`. Its default removes the child's `outline`; set it to `false` and define transparent chrome plus `:focus-visible` when the control needs its own focus ring. - Set `paused` for reduced motion. Also disable the wandering glow when its movement is unnecessary. - Keep the effect pointer-transparent and verify the child remains clickable and keyboard-operable. - Retain a plain CSS border or surface treatment for WebGL failure. ## Performance `metal-fx` reuses one shared WebGL context, one compiled shader, and one animation loop across mounted instances. It also pauses offscreen copies with `IntersectionObserver` and debounces resize work through animation frames. Still keep the effect selective: - Mount it on primary or stateful surfaces, not whole grids of idle controls. - Pause non-active instances. - Omit reflections unless they add visible depth. - Do not animate the wrapped element's dimensions continuously. - Prefer `disableGlow` for restrained cards and dense toolbars. ## Verification Run the project's lint, typecheck, production build, tests, and `git diff --check`. Then verify in the Codex browser: 1. Confirm the package is present in the manifest and lockfile. 2. Confirm one `.metal-fx-root` wraps one semantic host. 3. Test hover, pointer-down, selected, keyboard focus, and disabled states. 4. Confirm the active state is exposed semantically and remains clear with the effect hidden. 5. Confirm the focus ring is visible when `normalizeHostStyles` is enabled or disabled. 6. Check the measured size and radius at every responsive breakpoint. 7. Test the app's real light and dark themes; do not assume `auto` matches a manual toggle. 8. Enable reduced motion and confirm the shader stops moving. 9. Scroll the control offscreen and back; confirm the effect resumes without a blank frame. 10. Check console output for WebGL, hydration, resize, or ref errors. 11. Confirm neighbouring controls receive reflections only when explicitly targeted in dark mode. ## Failure Diagnosis - **The ring is covered:** the child has an opaque background above the canvas. Use default normalization or make the child transparent and style the wrapper. - **The focus ring disappeared:** `normalizeHostStyles` removed the child outline. Set it to `false` and reset only background, border, and shadow in project CSS. - **The radius is wrong:** use a real computed radius on the child or pass `borderRadius` explicitly. - **A circle looks soft or thin:** use `variant="circle"` and explicit square dimensions before increasing `ringCssPx`. - **The pattern is too busy:** lower `strength`, disable glow, or use `silver`; do not immediately thicken the ring. - **Reflections are missing in light mode:** expected behavior; reflections are dark-mode only. - **The effect is the wrong physical size after zooming:** set `scale` to the UI scale instead of tuning each pixel constant independently. - **The page crashes in a server component:** move the usage behind the framework's client-component boundary. - **The border is blank on unsupported hardware:** keep the semantic control and CSS fallback complete; treat the WebGL ring as enhancement. ## Handoff Report the installed `metal-fx` version, wrapped control, active-state source, preset, theme strategy, fallback border, reduced-motion behavior, build/test results, and Codex-browser verification. Distinguish local readiness from a deployed result.