metal-fx

repository·main·Indexed 42 days ago

https://github.com/jakubantalik/metal-fx

A React library that provides an animated WebGL 'liquid metal' effect for UI components like buttons and icons. It features a shared WebGL renderer for performance, proximity reflections in dark mode, and customizable color presets including chromatic, silver, and gold. Version 1.0.4.

Tokens
8.6K
Snippets
35
Records
46
Agent score
55%

What's inside metal-fx

  1. MetalFx performance and SSR characteristics

    main

    Performance

    • Shared Context: Uses a single shared WebGL context across all mounted instances.
    • Single Loop: A single requestAnimationFrame loop drives all instances.
    • Visibility Optimization: Uses IntersectionObserver to pause per-instance copies when offscreen. If all instances are offscreen, the GL render loop is skipped.
    • Resource Management: The GL context, program, and buffer are released automatically when the last instance unmounts.

    Server-Side Rendering (SSR)

    MetalFx is SSR-safe. It renders a transparent placeholder during SSR and only mounts the WebGL pipeline after client-side hydration, preventing flashes of broken effects or SSR errors.

  2. Quick start with MetalFx

    main

    The MetalFx component wraps a single child host element (like a button, chip, or icon). It measures the child and paints an animated metal ring on top. The child remains fully interactive because the overlay uses pointer-events: none.

    import { MetalFx } from 'metal-fx';
    
    function App() {
      return (
        <MetalFx variant="button">
          <button className="upgrade-pill">Upgrade to Pro</button>
        </MetalFx>
      );
    }
  3. Manage MetalFx sizing and layout

    main

    MetalFx is an inline-flex wrapper that sizes itself to its child. You have two patterns for controlling dimensions:

    Pattern 1: Size the child (Recommended) Style the child element directly using CSS or inline styles. The wrapper will expand to fit it.

    Pattern 2: Size the wrapper If you want the metal frame to be larger than the child (e.g., to add padding), apply styles to the MetalFx component and set the child to fill the container (width: 100%, height: 100%).

    // Pattern 1: size the child
    <MetalFx variant="circle">
      <button style={{ width: 36, height: 36 }} aria-label="Send">↑</button>
    </MetalFx>
    
    // Pattern 2: size the wrapper, child fills
    <MetalFx style={{ width: 36, height: 36 }} variant="circle">
      <button style={{ width: '100%', height: '100%' }} aria-label="Send">↑</button>
    </MetalFx>
  4. How MetalFx rendering works

    main

    MetalFx uses a shared WebGL renderer to drive multiple visual instances efficiently. The architecture follows these steps:

    1. Shared GL Canvas: A single offscreen WebGL canvas renders a plasma shader.
    2. Visible Instances: Each MetalFxInstance owns its own visible 2D canvas. This canvas receives a cropped and scaled copy of the shared GL output, applying an inner "hole punch" mask to create a ring effect.
    3. Glow Sampling: To optimize performance, glow sampling reads from a shared pixel buffer (gl.readPixels) that is refreshed at most every 200ms, preventing frequent GPU pipeline flushes.
    4. Animation Loop: The animation loop is capped at approximately 30fps, as the slow plasma motion and blur effects make higher frame rates imperceptible.

    This design allows many high-quality visual elements to coexist without the overhead of multiple heavy WebGL contexts.

  5. Use proximity reflection with reflectionTargets

    main

    The reflectionTargets prop allows you to pass an array of RefObject<HTMLElement> representing sibling elements. These elements will receive a soft proximity reflection of the metal effect.

    Important constraints:

    • Reflections only render when the resolved theme is 'dark'. In 'light' mode, this prop is a no-op (no DOM scanning or per-frame work occurs).
    • This is useful for elements like chips next to a button or a search field next to an upgrade pill.
    const siblingRef = useRef<HTMLElement>(null);
    
    <MetalFx 
      reflectionTargets={[siblingRef]} 
    >
      <button>Action</button>
    </MetalFx>
    
    <div ref={siblingRef}>I will receive a reflection in dark mode</div>
  6. Understand the Glow effect lifecycle

    main

    The glow effect follows a specific lifecycle to maintain visual fidelity:

    1. Injection: injectGlow creates the SVG markup and calculates the perimeter sampling table.
    2. Luminance Tracking: During updateGlow, the engine samples the shader's luminance at multiple points around the perimeter to find the 'brightest' spot.
    3. State Machine:
      • Dwell: The glow stays at a hotspot for a minimum duration (MIN_DWELL_MS).
      • Relocation: If a different point becomes significantly brighter, the glow fades out and transitions to the new location.
      • Wander: The hotspot subtly 'wanders' around the perimeter to prevent a static look.
    4. Color Tinting: The stroke color of the halo is dynamically tinted to match the shader color at the current hotspot.
    5. Resizing: If the container dimensions change, resizeGlow must be called to re-calculate the geometry.
  7. Enable proximity reflections in MetalFx

    main

    To enable proximity reflections, you must be in dark mode (or auto when the system is in dark mode) and provide an array of reflectionTargets. These targets are elements that will reflect the metallic effect as the user interacts with the page.

    Note: Reflections are a dark-mode only feature.

    import { useRef } from 'react';
    import { MetalFx } from './MetalFx';
    
    function App() {
      const targetRef = useRef<HTMLDivElement>(null);
    
      return (
        <div>
          <div ref={targetRef} style={{ width: 100, height: 100, background: 'black' }} />
          
          <MetalFx 
            theme="dark" 
            reflectionTargets={[targetRef]} 
          >
            <button>Reflective Button</button>
          </MetalFx>
        </div>
      );
    }
  8. Adjust effect strength and pause animation

    main

    Use strength to control the intensity and paused to freeze the animation:

    • strength: A number from 0 (invisible) to 1 (full intensity, default). It scales the canvas and glow opacity without altering the shader animation.
    • paused: A boolean prop. When present, it freezes the shader on its current frame while keeping the metal silhouette visible.
    // 70% effect intensity
    <MetalFx strength={0.7}>
      <button>Upgrade to Pro</button>
    </MetalFx>
    
    // Freeze the shader
    <MetalFx paused>
      <button>Upgrade to Pro</button>
    </MetalFx>
  9. Override custom border radius

    main

    By default, MetalFx automatically detects and reads the computed border-radius of the wrapped child. If you need to force a specific radius, use the borderRadius prop.

    <MetalFx borderRadius={20}>
      <button>Upgrade to Pro</button>
    </MetalFx>
  10. Apply color presets to MetalFx

    main

    Use the preset prop to choose from three bundled color palettes. Each preset includes tuned dark and light mode versions:

    • preset="chromatic": Iridescent rainbow (default).
    • preset="silver": Cool steel.
    • preset="gold": Warm gold.
    <MetalFx preset="chromatic" />
    <MetalFx preset="silver" />
    <MetalFx preset="gold" />
  11. Use proximity reflection in dark mode

    main

    In dark mode, you can pass an array of React refs to the reflectionTargets prop. The elements referenced will receive a soft, mirrored reflection of the MetalFx ring.

    Note: Reflections are automatically skipped when the resolved theme is light to save performance.

    const sendRef = useRef<HTMLButtonElement>(null);
    const chipRef = useRef<HTMLButtonElement>(null);
    
    <>
      <button ref={chipRef}>Tools</button>
      <MetalFx variant="circle" reflectionTargets={[chipRef]}>
        <button ref={sendRef} aria-label="Send">↑</button>
      </MetalFx>
    </>