Components/MetalFx

MetalFx

WebGL2 liquid metal for buttons, text, and badges.

VerifiedSince 0.6.2

Usage

Basic

TxMetalFx wraps exactly one host element and paints a metal ring over it on a WebGL2 canvas.

Loading demo...

Variants

button is a pill with a 1 px ring and circle a true circle with a 2 px ring; the ring radius comes from the child's border-radius.

<template>
  <TxMetalFx variant="button" preset="silver" :strength="0.7">
    <a href="/pricing">Upgrade to Pro</a>
  </TxMetalFx>

  <TxMetalFx variant="circle" preset="gold" inner-shadow :scale="1.5">
    <button type="button" aria-label="Send">↑</button>
  </TxMetalFx>
</template>

Text and Badges

TxMetalText and TxMetalBadge always use the chromatic material and set aria-label to their own text.

<template>
  <TxMetalText font="600 32px/1.1 Inter, sans-serif" color="#e8e8e8">Pro</TxMetalText>
  <TxMetalBadge>Beta</TxMetalBadge>
</template>

Best Practices

  • Give icon-only children an explicit width and height; the wrapper is inline-flex and sizes to the child.
  • Keep the child's background transparent; put a custom fill on TxMetalFx itself.
  • Use one preset and theme per page: every instance shares one material, and the last one applied wins.
  • Keep metal elements apart; two metal buttons side by side compete for attention.
  • Pass reflectionTargets only for stable neighbours you can hold a reference to.

API Reference

TxMetalFx

Props

PropTypeDefaultDescription
variant'button' | 'circle''button'Ring baseline: pill at 1 px, circle at 2 px.
preset'chromatic' | 'silver' | 'gold''chromatic'Metal colour; each ships a dark and a light tuning.
theme'auto' | 'dark' | 'light''auto'Picks the preset's dark or light side; auto follows the system live.
strengthnumber1Scales shader opacity and glow alpha together (0-1).
glowGainnumber1Extra multiplier on the glow only, clamped to 0-1.
pausedbooleanfalseFreezes the instance on its current frame.
borderRadiusnumberauto-detectedRing radius in CSS px.
normalizeHostStylesbooleantrueStrips the child's border, outline, and box-shadow.
reflectionTargetsArray<Element | { ref, strength }>noneNeighbours that catch a mirrored reflection.
disableGlowbooleanfalseRemoves the wandering halo; the ring still renders.
innerShadowboolean | { offsetY, blur, alpha, color }offLight rim along the ring's top inside edge.
shaderScalenumbervariant baseline × scaleOverrides the shader sampling scale.
ringCssPxnumbervariant baseline × scaleOverrides the ring thickness in CSS px.
scalenumber1Master multiplier on every absolute-pixel constant in the engine.
mask(ctx, size) => voidnoneCustom alpha-mask painter; the shader shows only where it paints.
glowMode'mask' | 'ring''mask'Glow placement when mask is set.

Slots

SlotPropsDescription
default-Exactly one host element; it keeps its own event handlers.

CSS Variables

VariableSourceDescription
--mfx-strengthstrengthStrength clamped to 0-1, for downstream CSS to read.
--mfx-radiusborderRadius or auto-detectedRing radius in px.

TxMetalText

Props

PropTypeDefaultDescription
fontstringrequiredCSS font shorthand.
colorstringrequiredBase text colour behind the metal.
strengthnumber1Metal opacity.
reflectionTargetsArray<Element | { ref, strength }>noneNeighbours that catch the metal.
childrenstringnoneText used when the default slot is empty.
theme'auto' | 'dark' | 'light'nonePicks the preset's dark or light side; follows the system when unset.
innerShadowTextInnerShadow | nullFIGMA_INNER_SHADOWLight rim along the top inside edge of the glyphs; null turns it off.
glowbooleanfalseShows a halo on the glyphs.
glowGainnumber2.5Halo multiplier while glow is on.
metalOpacitynumber0.62How much metal shows over the base colour (0-1), multiplied by strength.
shaderScalenumber2.8Zoom of the metal inside the glyphs.

Slots

SlotPropsDescription
default-The label text.

TxMetalBadge

Props

PropTypeDefaultDescription
childrenstring'New'Text used when the default slot is empty.
strengthnumber1Metal opacity.
theme'auto' | 'dark' | 'light'nonePicks the preset's dark or light side; follows the system when unset.
scalenumber1Size multiplier on the 45×25 base.
reflectionTargetsArray<Element | { ref, strength }>noneNeighbours that catch the metal.
metalOpacitynumber0.8How much metal shows over the white fill (0-1), multiplied by strength.
shaderScalenumber1.6Zoom of the metal texture.
coreMetalBadgeCore{ r: 46, blur: 100, a: 0.94, size: 49 }Radius, ramp, opacity, and size of the white core under the label.
gradientnumber0Strength of the top-to-bottom white wash (0-1).
glownumber0.41Strength of the inner white glow (0-1).
textColorstring'#323232'Label colour.

Slots

SlotPropsDescription
default-The label text; falls back to children when empty.

Overview

  • All instances share one WebGL2 context, one material, and one loop at about 15 fps; offscreen instances (IntersectionObserver, 64 px margin) are skipped, and the loop stops while the tab is hidden.
  • The wrapper, child included, stays invisible until the first metal frame paints; don't measure or animate the child before then.
  • Reflections render only in dark theme; light theme does no DOM scan and no per-frame work.
  • The shader ignores reduced motion; pass paused (and optionally disableGlow) yourself.
  • The cursor reflection needs a sprite registered through setCursorSprite and stays off without one; it also turns off under reduced motion, forced-colors, and coarse pointers.
  • Without WebGL2, the child keeps its own styles and renders inside div.metal-fx-fallback[data-metal-fx-unsupported], with no ring.

Technologies

  • engine/** is a verbatim port of Jakubantalik/Libraries · metal-fx v2 (MIT © Jakub Antalik); the Vue shells mirror the React wrappers' lifecycle, measurement, and cleanup.
  • Page-level tuning uses the engine primitives exported from the module: setGlowConfig, setCursorLightConfig, setBendConfig, isMetalFxSupported, and more.
  • Source: packages/tuffex/packages/components/src/metal-fx/.
查看源码
packages/tuffex/packages/components/src/metal-fx/index.ts