Sinua
Connect your app

Bindings

Map any number in your app onto a value or a material, with ranges and easing curves — in the design file, not in your code.

A binding turns one of your app's numbers (an input) into a value the visual draws (a target). Steps become ring progress, a heart rate becomes glow, a mic level becomes the orb's breathing. The mapping lives in the FX Spec, so a designer can tune it without a code change.

activity-rings.fxspec.json
{
  "fxSpec": "1.8",
  "name": "Activity rings",
  "object": "ring",
  "pattern": "tracking",
  "params": {
    "ringCount": 3
  },
  "bindings": {
    "progress[0]": {
      "input": "steps",
      "inputRange": [
        0,
        10000,
        30000
      ],
      "outputRange": [
        0,
        1,
        3
      ],
      "curve": [
        "easeOut",
        "linear"
      ]
    },
    "progress[1]": {
      "input": "waterMl",
      "inputRange": [
        0,
        2500
      ]
    },
    "progress[2]": {
      "input": "activeMinutes",
      "inputRange": [
        0,
        30
      ]
    },
    "glow.strength": {
      "input": "heartRate",
      "inputRange": [
        60,
        100,
        160
      ],
      "outputRange": [
        0,
        0.2,
        0.8
      ],
      "curve": [
        "linear",
        "easeIn"
      ]
    }
  },
  "states": {
    "goalReached": {
      "pattern": "completing",
      "params": {
        "progress": 1
      },
      "bindings": {
        "progress[0]": null,
        "progress[1]": null,
        "progress[2]": null
      }
    }
  }
}
activity-rings

Anatomy

Each entry in bindings is keyed by its target (above: progress[0]) and says:

KeyMeaning
target (the key)What to drive. One of the bindable targets below.
inputYour name for the number. Pass it at runtime in inputs. Any name works.
inputRangeInput stops, ascending or descending, at least two. Default [0, 1].
outputRangeOutput stops, same count as inputRange. Default: the target's natural range.
curvelinear, ease, easeIn, easeOut or easeInOut (CSS timing curves), one for all segments or one per segment. In TypeScript, a function works too. Default linear.

The output is always clamped to the target's range. Multi-stop ranges let one input do different things in different spans. Above, the first 10,000 steps fill the ring, and the next 20,000 add two more laps.

Bindable targets

Values and the material masters: progress, progress[0]…progress[3], quality, accuracy, audioLevel, muted, glow.strength, noise.strength, gradient.strength, pulse.strength, color.mix. Materials that need a companion setting to be visible get a sensible one automatically.

Bindings and states

Bindings are part of a state's patch, like everything else. A state can add a binding, change one, or remove one with null. The goalReached state above drops the per-ring bindings and shows a full completion ring instead.

An input that isn't passed

A binding whose input you don't pass leaves its target at the design's static value, and the view reports it as inactive. That's how one file works for users who have a heart-rate sensor and for users who don't.

Binding in code

Without an FX Spec, bindReactiveInput applies the same mapping in TypeScript:

bind-reactive-input.ts
import { bindReactiveInput, frameWithOverrides } from "@sinua/core";

declare const steps: number; // your app's value
declare const t: number; // seconds since the view started

// 10,000 steps fills the ring, eased out so the last few thousand feel slower.
const overrides = bindReactiveInput({
  value: steps,
  target: "progress",
  input: [0, 10_000],
  curve: "easeOut",
});
const frame = frameWithOverrides("completing", 64, t, overrides);

On every platform, pass the spec and your values as inputs:

react.tsx
import { SinuaView } from "@sinua/web/react";
import rings from "../spec/activity-rings.fxspec.json";

// The mapping lives in the spec's `bindings`; the app passes only its raw numbers.
// An input you leave out keeps its target at the design's value (reported as inactive).
export function DailyRings({ steps, waterMl, activeMinutes }: { steps: number; waterMl: number; activeMinutes: number }) {
  return <SinuaView spec={rings} inputs={{ steps, waterMl, activeMinutes }} style={{ width: 120, height: 120 }} />;
}

On this page