> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Visual effects

> Geometry-aware visual transforms and named coordinate spaces

export const PlatformStatuses = ({statuses}) => {
  const StatusBadge = ({status, label}) => {
    const styles = {
      green: {
        backgroundColor: '#dcfce7',
        color: '#166534'
      },
      orange: {
        backgroundColor: '#fed7aa',
        color: '#9a3412'
      },
      red: {
        backgroundColor: '#fecaca',
        color: '#991b1b'
      },
      gray: {
        backgroundColor: '#f3f4f6',
        color: '#4b5563'
      }
    };
    const baseStyle = {
      display: 'inline-flex',
      alignItems: 'center',
      padding: '0.125rem 0.625rem',
      borderRadius: '9999px',
      fontSize: '0.875rem',
      fontWeight: '500'
    };
    const colorStyle = styles[status] || styles.green;
    return <span style={{
      ...baseStyle,
      ...colorStyle
    }}>
        {label || status}
      </span>;
  };
  const STATUS_CONFIG = {
    supported: {
      label: "Supported",
      color: "green"
    },
    partial: {
      label: "Partial",
      color: "orange"
    },
    "not-implemented": {
      label: "Not Implemented",
      color: "gray"
    }
  };
  const renderCard = (platform, value) => {
    if (!value) return null;
    const {status, note} = typeof value === "string" ? {
      status: value
    } : value;
    const config = STATUS_CONFIG[status];
    if (!config) return null;
    const titleMap = {
      ios: "SwiftUI",
      android: "Jetpack Compose",
      web: "Web"
    };
    return <Card key={platform} title={titleMap[platform] || platform}>
          <StatusBadge status={config.color} label={config.label} />
          {note && <div style={{
      marginTop: '0.5rem',
      fontSize: '0.875rem',
      color: '#6b7280'
    }}>
              {note}
            </div>}
      </Card>;
  };
  if (statuses == null) {
    return null;
  }
  return <Columns cols="3">
      {Object.entries(statuses).map(([platform, value]) => renderCard(platform, value))}
    </Columns>;
};

export const ComposeJS = ({code, name, height}) => {
  const encodedCode = useMemo(() => {
    if (!code) return "";
    try {
      return btoa(code);
    } catch (e) {
      console.error("Failed to encode code", e);
      return "";
    }
  }, [code]);
  if (!encodedCode) {
    return null;
  }
  return <iframe src={`https://www.metabind.ai/embed?code=${encodedCode}&name=${name ?? 'Example'}`} loading="lazy" style={{
    width: "100%",
    height: height || '350px',
    border: "1px solid #e5e7eb",
    borderRadius: "var(--rounded-2xl,1rem)",
    overflow: "hidden"
  }} title="ComposeJS Preview" />;
};

`visualEffect` applies geometry-driven transforms — opacity, offset, scale, blur, rotation — that update as a component's position or size changes, ideal for parallax and scroll-driven UI. `coordinateSpace` registers a named frame of reference that `visualEffect` and `GeometryReader` can resolve positions against.

Effects applied through `visualEffect` are purely visual and do not affect layout. Pair `coordinateSpace` on a `ScrollView` with `visualEffect` on its descendants to drive effects from scroll position.

## visualEffect

Applies geometry-aware visual effects using a builder pattern.

```typescript theme={null}
.visualEffect(callback: (builder: VisualEffectBuilder, proxy: GeometryProxy) => VisualEffectBuilder): Component
```

<ParamField path="callback" type="(builder: VisualEffectBuilder, proxy: GeometryProxy) => VisualEffectBuilder" required>
  A function that receives a `VisualEffectBuilder` and a `GeometryProxy`, and returns the builder with effects chained onto it. The proxy provides layout information (size, position, safe area insets) that can drive the effect values.

  <Expandable title="VisualEffectBuilder methods">
    * `blur(radius)` — applies a Gaussian blur
    * `opacity(amount)` — sets opacity (0 to 1)
    * `offset({ x, y })` — offsets the component
    * `scale(value)` or `scale({ x, y })` — scales uniformly or per-axis
    * `transform(matrix)` — applies a full 2D affine transform
    * `translation({ x, y })` — translates the component
    * `rotation(degrees)`, `rotation({ degrees })`, or `rotation({ radians })` — rotates the component
  </Expandable>

  <Expandable title="GeometryProxy properties">
    * `size` — `{ width, height }` of the component
    * `safeAreaInsets` — `{ top, leading, bottom, trailing }`
    * `frame(coordinateSpace)` — returns a `GeometryRect` with `minX`, `minY`, `maxX`, `maxY`, `width`, `height`, `midX`, `midY`
    * `bounds(coordinateSpace)` — returns the bounds of a named coordinate space

    Available coordinate spaces: `"global"`, `"local"`, `"scrollView"`, or a custom name registered via `.coordinateSpace()`.
  </Expandable>
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "partial", note: "Registered but handler not invoked; no visual effects are applied" },
web: { status: "supported" },
}}
/>

**Scroll-driven opacity**

Fade a component based on its position within a scroll view.

```typescript theme={null}
Text("Fading text")
    .visualEffect((builder, proxy) =>
        builder.opacity(
            proxy.frame("scrollView").minY > 0 ? 1 : 0.3
        )
    )
```

**Parallax effect**

```typescript theme={null}
Image({ url: "banner.jpg" })
    .resizable()
    .scaledToFill()
    .frame({ height: 300 })
    .clipped()
    .visualEffect((builder, proxy) =>
        builder.offset({
            x: 0,
            y: proxy.frame("scrollView").minY * 0.5
        })
    )
```

**Scale and blur on scroll**

```typescript theme={null}
Text("Hero Title")
    .font("largeTitle")
    .visualEffect((builder, proxy) => {
        const scrollY = proxy.frame("scrollView").minY
        return builder
            .scale(scrollY > 0 ? 1 + scrollY / 500 : 1)
            .blur(scrollY < 0 ? -scrollY / 10 : 0)
    })
```

**Size-based rotation**

```typescript theme={null}
Rectangle()
    .frame({ width: 100, height: 100 })
    .foregroundStyle(Color("blue"))
    .visualEffect((builder, proxy) =>
        builder.rotation(proxy.size.width > 100 ? 45 : 0)
    )
```

The callback is re-evaluated whenever the component's geometry changes (e.g., during scrolling or resizing). Effects applied through the builder do not affect the component's layout — they are purely visual transforms. Use [coordinateSpace](#coordinatespace) on a parent to create named coordinate spaces that can be referenced in `proxy.frame()`.

## coordinateSpace

Assigns a named coordinate space to a component for geometry calculations.

```typescript theme={null}
.coordinateSpace(name: string)
```

<ParamField path="name" type="string" required>
  A unique name for this coordinate space. Other components can reference this name in `GeometryProxy.frame()` to get their position relative to this component.
</ParamField>

<PlatformStatuses
  statuses={{
ios: { status: "supported" },
android: { status: "partial", note: "Modifier registered but does not track coordinate space" },
web: "not-implemented",
}}
/>

**Named scroll view coordinate space**

```typescript theme={null}
ScrollView([
    VStack(items.map((item) =>
        Text(item.name)
            .visualEffect((builder, proxy) =>
                builder.opacity(
                    proxy.frame("scroll").minY > 0 ? 1 : 0.3
                )
            )
    ))
])
    .coordinateSpace("scroll")
```

**Track position relative to a container**

```typescript theme={null}
VStack([
    GeometryReader((proxy) =>
        Text("Y: " + String(proxy.frame("container").minY))
    )
])
    .coordinateSpace("container")
```

## See also

* [offset](/bindjs/modifiers/offset) — translate a component without affecting layout
* [scaleEffect](/bindjs/modifiers/scaleEffect) — static scaling without geometry feedback
* [rotationEffect](/bindjs/modifiers/rotationEffect) — static rotation
* [blur](/bindjs/modifiers/blur) — apply a Gaussian blur
* [opacity](/bindjs/modifiers/opacity) — set transparency
