---
title: "MetalFx"
description: "WebGL2 liquid metal for buttons, text, and badges."
category: Effects
status: beta
since: 0.6.2
tags: [metal, webgl, button, badge, cta, reflection]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
`TxMetalFx` wraps exactly one host element and paints a metal ring over it on a WebGL2 canvas.
:::TuffDemoWrapper{demo="MetalFxShowcaseDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { computed } from 'vue'

  const colorMode = useColorMode()
  const metalTheme = computed(() => (colorMode.value === 'dark' ? 'dark' : 'light'))
  </script>

  <template>
    <TxMetalFx preset="chromatic" :theme="metalTheme">
      <a href="#" class="cta">Upgrade to Pro</a>
    </TxMetalFx>

    <TxMetalFx variant="circle" preset="chromatic" :theme="metalTheme" inner-shadow :strength="0.9">
      <button type="button" aria-label="Send" style="width: 40px; height: 40px; border-radius: 999px">↑</button>
    </TxMetalFx>

    <span>
      Plan
      <TxMetalText font="500 28px/1.2 Inter, system-ui, sans-serif" color="#e2e2e2">Pro</TxMetalText>
    </span>

    <TxMetalBadge>New</TxMetalBadge>
  </template>
---
:::

### 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`.

```vue
<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.

```vue
<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

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `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. |
| `strength` | `number` | `1` | Scales shader opacity and glow alpha together (0-1). |
| `glowGain` | `number` | `1` | Extra multiplier on the glow only, clamped to 0-1. |
| `paused` | `boolean` | `false` | Freezes the instance on its current frame. |
| `borderRadius` | `number` | auto-detected | Ring radius in CSS px. |
| `normalizeHostStyles` | `boolean` | `true` | Strips the child's border, outline, and box-shadow. |
| `reflectionTargets` | `Array<Element \| { ref, strength }>` | none | Neighbours that catch a mirrored reflection. |
| `disableGlow` | `boolean` | `false` | Removes the wandering halo; the ring still renders. |
| `innerShadow` | `boolean \| { offsetY, blur, alpha, color }` | off | Light rim along the ring's top inside edge. |
| `shaderScale` | `number` | variant baseline × `scale` | Overrides the shader sampling scale. |
| `ringCssPx` | `number` | variant baseline × `scale` | Overrides the ring thickness in CSS px. |
| `scale` | `number` | `1` | Master multiplier on every absolute-pixel constant in the engine. |
| `mask` | `(ctx, size) => void` | none | Custom alpha-mask painter; the shader shows only where it paints. |
| `glowMode` | `'mask' \| 'ring'` | `'mask'` | Glow placement when `mask` is set. |

#### Slots

| Slot | Props | Description |
|------|------|-------------|
| `default` | - | Exactly one host element; it keeps its own event handlers. |

#### CSS Variables

| Variable | Source | Description |
|------|------|-------------|
| `--mfx-strength` | `strength` | Strength clamped to 0-1, for downstream CSS to read. |
| `--mfx-radius` | `borderRadius` or auto-detected | Ring radius in px. |

### TxMetalText

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `font` | `string` | required | CSS `font` shorthand. |
| `color` | `string` | required | Base text colour behind the metal. |
| `strength` | `number` | `1` | Metal opacity. |
| `reflectionTargets` | `Array<Element \| { ref, strength }>` | none | Neighbours that catch the metal. |
| `children` | `string` | none | Text used when the default slot is empty. |
| `theme` | `'auto' \| 'dark' \| 'light'` | none | Picks the preset's dark or light side; follows the system when unset. |
| `innerShadow` | `TextInnerShadow \| null` | `FIGMA_INNER_SHADOW` | Light rim along the top inside edge of the glyphs; `null` turns it off. |
| `glow` | `boolean` | `false` | Shows a halo on the glyphs. |
| `glowGain` | `number` | `2.5` | Halo multiplier while `glow` is on. |
| `metalOpacity` | `number` | `0.62` | How much metal shows over the base colour (0-1), multiplied by `strength`. |
| `shaderScale` | `number` | `2.8` | Zoom of the metal inside the glyphs. |

#### Slots

| Slot | Props | Description |
|------|------|-------------|
| `default` | - | The label text. |

### TxMetalBadge

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `string` | `'New'` | Text used when the default slot is empty. |
| `strength` | `number` | `1` | Metal opacity. |
| `theme` | `'auto' \| 'dark' \| 'light'` | none | Picks the preset's dark or light side; follows the system when unset. |
| `scale` | `number` | `1` | Size multiplier on the 45×25 base. |
| `reflectionTargets` | `Array<Element \| { ref, strength }>` | none | Neighbours that catch the metal. |
| `metalOpacity` | `number` | `0.8` | How much metal shows over the white fill (0-1), multiplied by `strength`. |
| `shaderScale` | `number` | `1.6` | Zoom of the metal texture. |
| `core` | `MetalBadgeCore` | `{ r: 46, blur: 100, a: 0.94, size: 49 }` | Radius, ramp, opacity, and size of the white core under the label. |
| `gradient` | `number` | `0` | Strength of the top-to-bottom white wash (0-1). |
| `glow` | `number` | `0.41` | Strength of the inner white glow (0-1). |
| `textColor` | `string` | `'#323232'` | Label colour. |

#### Slots

| Slot | Props | Description |
|------|------|-------------|
| `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](https://github.com/Jakubantalik/Libraries/tree/main/packages/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/`.

<TuffDocSourceLink />
