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

# Reactive Binding

> A slider driving a ring, progress bars, and text in real time.

export const ComponentPreview = ({json, height, resizable, bare, hideJson, playground, children}) => {
  const hostRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [playgroundHref, setPlaygroundHref] = React.useState(null);
  React.useEffect(function () {
    if (!playground) return;
    var isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
    var path = isLocal ? "/playground" : "/docs/playground";
    var url = new URL(path, window.location.href);
    url.hash = "code=" + playground;
    setPlaygroundHref(url.href);
  }, [playground]);
  var jsonStr = typeof json === "string" ? json : JSON.stringify(json, null, 2);
  React.useEffect(function () {
    if (cardRef.current) {
      var card = cardRef.current.closest(".group");
      if (card) card.classList.remove("group");
    }
    var host = hostRef.current;
    if (!host || !json) return;
    function mount() {
      if (!window.__prefab || !host) return;
      var dark = document.documentElement.classList.contains("dark");
      handleRef.current = window.__prefab.mountPreview(host, jsonStr, {
        dark: dark
      });
      if (bare && host.shadowRoot) {
        var m = host.shadowRoot.querySelector("[data-prefab-mount]");
        if (m) m.style.background = "transparent";
      }
    }
    if (window.__prefab) {
      mount();
    } else {
      if (!window.__prefabLoading) {
        if (!window.__prefabReady) {
          var s = document.createElement("script");
          s.src = "/renderer.js";
          document.head.appendChild(s);
        }
        window.__prefabLoading = new Promise(function (resolve) {
          function check() {
            if (window.__prefabReady) {
              window.__prefabReady.then(resolve);
            } else {
              setTimeout(check, 10);
            }
          }
          check();
        });
      }
      window.__prefabLoading.then(mount);
    }
    var observer = new MutationObserver(function () {
      var dark = document.documentElement.classList.contains("dark");
      if (handleRef.current) handleRef.current.setDark(dark);
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return function () {
      observer.disconnect();
      if (handleRef.current) {
        handleRef.current.unmount();
        handleRef.current = null;
      }
    };
  }, [json]);
  function startResize(e) {
    e.preventDefault();
    var startX = e.clientX;
    var startW = hostRef.current ? hostRef.current.offsetWidth : 0;
    var maxW = containerRef.current ? containerRef.current.offsetWidth - 10 : startW;
    setIsDragging(true);
    function onMove(ev) {
      var w = Math.max(260, Math.min(startW + (ev.clientX - startX), maxW));
      setPreviewWidth(w);
    }
    function onUp() {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      setIsDragging(false);
    }
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }
  if (bare) {
    return <div ref={hostRef} />;
  }
  if (resizable) {
    return <Card>
        <div ref={cardRef} />
        <div ref={containerRef} style={{
      display: "flex",
      position: "relative",
      overflow: "hidden"
    }}>
          <div ref={hostRef} style={{
      flex: previewWidth ? "none" : 1,
      width: previewWidth ? previewWidth + "px" : undefined,
      minWidth: 0
    }} />
          <div onMouseDown={startResize} style={{
      width: "14px",
      flexShrink: 0,
      cursor: "col-resize",
      background: isDragging ? "var(--border, #e5e7eb)" : "color-mix(in srgb, var(--border, #e5e7eb) 40%, transparent)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "background 0.15s",
      userSelect: "none"
    }}>
            <svg width="6" height="24" style={{
      opacity: 0.5
    }}>
              <circle cx="2" cy="4" r="1.2" fill="currentColor" />
              <circle cx="2" cy="9" r="1.2" fill="currentColor" />
              <circle cx="2" cy="14" r="1.2" fill="currentColor" />
              <circle cx="2" cy="19" r="1.2" fill="currentColor" />
              <circle cx="5" cy="4" r="1.2" fill="currentColor" />
              <circle cx="5" cy="9" r="1.2" fill="currentColor" />
              <circle cx="5" cy="14" r="1.2" fill="currentColor" />
              <circle cx="5" cy="19" r="1.2" fill="currentColor" />
            </svg>
          </div>
          <div style={{
      flex: previewWidth ? 1 : "0 0 24px",
      minWidth: "24px",
      background: "repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--border, #e5e7eb) 3px, var(--border, #e5e7eb) 4px)",
      opacity: 0.4
    }} />
          {previewWidth && <div style={{
      position: "absolute",
      bottom: "8px",
      right: "8px",
      background: "var(--background, white)",
      border: "1px solid var(--border, #e5e7eb)",
      borderRadius: "4px",
      padding: "1px 6px",
      fontSize: "11px",
      fontFamily: "monospace",
      color: "var(--muted-foreground, #6b7280)",
      pointerEvents: "none"
    }}>
              {previewWidth}px
            </div>}
        </div>
        {playgroundHref && <div style={{
      display: "flex",
      justifyContent: "flex-end",
      padding: "4px 0 8px"
    }}>
            <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
      fontSize: "11px",
      opacity: 0.4,
      textDecoration: "none",
      textDecorationLine: "none",
      borderBottom: "none",
      boxShadow: "none",
      display: "inline-flex",
      alignItems: "center",
      gap: "3px",
      transition: "opacity 0.15s",
      fontWeight: 400
    }} onMouseOver={function (e) {
      e.currentTarget.style.opacity = "0.6";
    }} onMouseOut={function (e) {
      e.currentTarget.style.opacity = "0.4";
    }}>
              Edit in Playground
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
            </a>
          </div>}
        <div style={{
      marginBottom: "-2rem"
    }}>{children}</div>
      </Card>;
  }
  return <Card>
      <div ref={cardRef} />
      <div ref={hostRef} />
      {playgroundHref && <div style={{
    display: "flex",
    justifyContent: "flex-end",
    padding: "4px 0 8px"
  }}>
          <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
    fontSize: "11px",
    opacity: 0.4,
    textDecoration: "none",
    textDecorationLine: "none",
    borderBottom: "none",
    boxShadow: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "3px",
    transition: "opacity 0.15s",
    fontWeight: 400
  }} onMouseOver={function (e) {
    e.currentTarget.style.opacity = "0.6";
  }} onMouseOut={function (e) {
    e.currentTarget.style.opacity = "0.4";
  }}>
            Edit in Playground
            <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
          </a>
        </div>}
      <div style={{
    marginBottom: "-2rem"
  }}>{children}</div>
    </Card>;
};

A single slider drives every other element on the page. The ring, four progress bars, and a text status all update instantly as you drag, demonstrating how `Rx` expressions compose through arithmetic, ternary conditionals, and f-strings.

<ComponentPreview json={{"view":{"cssClass":"w-full","type":"Card","children":[{"type":"CardHeader","children":[{"type":"CardTitle","content":"Power Level"}]},{"type":"CardContent","children":[{"cssClass":"gap-6","type":"Column","children":[{"name":"level","value":50.0,"type":"Slider","min":0.0,"max":100.0,"disabled":false,"size":"default"},{"cssClass":"gap-6 items-center","type":"Row","children":[{"type":"Ring","value":"{{ level }}","min":0,"max":100,"label":"{{ level }}%","variant":"{{ level > 75 ? 'destructive' : (level > 40 ? 'warning' : 'success') }}","size":"lg","thickness":10.0},{"cssClass":"gap-4 grid-cols-2 flex-1","type":"Grid","children":[{"cssClass":"gap-1","type":"Column","children":[{"content":"Primary","type":"Muted"},{"type":"Progress","value":"{{ level }}","max":100.0,"variant":"{{ level > 75 ? 'destructive' : (level > 40 ? 'warning' : 'success') }}","size":"default"}]},{"cssClass":"gap-1","type":"Column","children":[{"content":"Inverse","type":"Muted"},{"type":"Progress","value":"{{ 100 - level }}","max":100.0,"variant":"info","size":"default"}]},{"cssClass":"gap-1","type":"Column","children":[{"content":"Doubled","type":"Muted"},{"type":"Progress","value":"{{ level * 2 }}","max":100.0,"variant":"default","size":"default"}]},{"cssClass":"gap-1","type":"Column","children":[{"content":"Halved","type":"Muted"},{"type":"Progress","value":"{{ level / 2 }}","max":100.0,"variant":"success","size":"default"}]}]}]},{"cssClass":"text-sm text-muted-foreground text-center","content":"Level is {{ level }}% \u2014 {{ level > 75 ? 'critical!' : (level > 40 ? 'nominal' : 'low') }}","type":"Text"}]}]}]},"state":{"level":50.0}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgQ2FyZCwgQ2FyZENvbnRlbnQsIENhcmRIZWFkZXIsIENhcmRUaXRsZSwKICAgIENvbHVtbiwgR3JpZCwgTXV0ZWQsIFByb2dyZXNzLCBSaW5nLCBSb3csIFNsaWRlciwgVGV4dCwKKQpmcm9tIHByZWZhYl91aS5yeCBpbXBvcnQgUngKCmxldmVsID0gUngoImxldmVsIikKdmFyaWFudCA9IChsZXZlbCA-IDc1KS50aGVuKAogICAgImRlc3RydWN0aXZlIiwgKGxldmVsID4gNDApLnRoZW4oIndhcm5pbmciLCAic3VjY2VzcyIpCikKCndpdGggQ2FyZChjc3NfY2xhc3M9InctZnVsbCIpOgogICAgd2l0aCBDYXJkSGVhZGVyKCk6CiAgICAgICAgQ2FyZFRpdGxlKCJQb3dlciBMZXZlbCIpCiAgICB3aXRoIENhcmRDb250ZW50KCk6CiAgICAgICAgd2l0aCBDb2x1bW4oZ2FwPTYpOgogICAgICAgICAgICBTbGlkZXIobmFtZT0ibGV2ZWwiLCBtaW49MCwgbWF4PTEwMCwgdmFsdWU9NTApCiAgICAgICAgICAgIHdpdGggUm93KGdhcD02LCBhbGlnbj0iY2VudGVyIik6CiAgICAgICAgICAgICAgICBSaW5nKAogICAgICAgICAgICAgICAgICAgIHZhbHVlPWxldmVsLCBsYWJlbD1mIntsZXZlbH0lIiwKICAgICAgICAgICAgICAgICAgICB2YXJpYW50PXZhcmlhbnQsIHNpemU9ImxnIiwgdGhpY2tuZXNzPTEwLAogICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgd2l0aCBHcmlkKGNvbHVtbnM9MiwgZ2FwPTQsIGNzc19jbGFzcz0iZmxleC0xIik6CiAgICAgICAgICAgICAgICAgICAgd2l0aCBDb2x1bW4oZ2FwPTEpOgogICAgICAgICAgICAgICAgICAgICAgICBNdXRlZCgiUHJpbWFyeSIpCiAgICAgICAgICAgICAgICAgICAgICAgIFByb2dyZXNzKHZhbHVlPWxldmVsLCBtYXg9MTAwLCB2YXJpYW50PXZhcmlhbnQpCiAgICAgICAgICAgICAgICAgICAgd2l0aCBDb2x1bW4oZ2FwPTEpOgogICAgICAgICAgICAgICAgICAgICAgICBNdXRlZCgiSW52ZXJzZSIpCiAgICAgICAgICAgICAgICAgICAgICAgIFByb2dyZXNzKHZhbHVlPTEwMCAtIGxldmVsLCBtYXg9MTAwLCB2YXJpYW50PSJpbmZvIikKICAgICAgICAgICAgICAgICAgICB3aXRoIENvbHVtbihnYXA9MSk6CiAgICAgICAgICAgICAgICAgICAgICAgIE11dGVkKCJEb3VibGVkIikKICAgICAgICAgICAgICAgICAgICAgICAgUHJvZ3Jlc3ModmFsdWU9bGV2ZWwgKiAyLCBtYXg9MTAwLCB2YXJpYW50PSJkZWZhdWx0IikKICAgICAgICAgICAgICAgICAgICB3aXRoIENvbHVtbihnYXA9MSk6CiAgICAgICAgICAgICAgICAgICAgICAgIE11dGVkKCJIYWx2ZWQiKQogICAgICAgICAgICAgICAgICAgICAgICBQcm9ncmVzcyh2YWx1ZT1sZXZlbCAvIDIsIG1heD0xMDAsIHZhcmlhbnQ9InN1Y2Nlc3MiKQogICAgICAgICAgICBUZXh0KAogICAgICAgICAgICAgICAgZiJMZXZlbCBpcyB7bGV2ZWx9JSDigJQgIgogICAgICAgICAgICAgICAgZiJ7KGxldmVsID4gNzUpLnRoZW4oJ2NyaXRpY2FsIScsIChsZXZlbCA-IDQwKS50aGVuKCdub21pbmFsJywgJ2xvdycpKX0iLAogICAgICAgICAgICAgICAgY3NzX2NsYXNzPSJ0ZXh0LXNtIHRleHQtbXV0ZWQtZm9yZWdyb3VuZCB0ZXh0LWNlbnRlciIsCiAgICAgICAgICAgICkK">
  <CodeGroup>
    ```python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components import (
        Card, CardContent, CardHeader, CardTitle,
        Column, Grid, Muted, Progress, Ring, Row, Slider, Text,
    )
    from prefab_ui.rx import Rx

    level = Rx("level")
    variant = (level > 75).then(
        "destructive", (level > 40).then("warning", "success")
    )

    with Card(css_class="w-full"):
        with CardHeader():
            CardTitle("Power Level")
        with CardContent():
            with Column(gap=6):
                Slider(name="level", min=0, max=100, value=50)
                with Row(gap=6, align="center"):
                    Ring(
                        value=level, label=f"{level}%",
                        variant=variant, size="lg", thickness=10,
                    )
                    with Grid(columns=2, gap=4, css_class="flex-1"):
                        with Column(gap=1):
                            Muted("Primary")
                            Progress(value=level, max=100, variant=variant)
                        with Column(gap=1):
                            Muted("Inverse")
                            Progress(value=100 - level, max=100, variant="info")
                        with Column(gap=1):
                            Muted("Doubled")
                            Progress(value=level * 2, max=100, variant="default")
                        with Column(gap=1):
                            Muted("Halved")
                            Progress(value=level / 2, max=100, variant="success")
                Text(
                    f"Level is {level}% — "
                    f"{(level > 75).then('critical!', (level > 40).then('nominal', 'low'))}",
                    css_class="text-sm text-muted-foreground text-center",
                )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "w-full",
        "type": "Card",
        "children": [
          {
            "type": "CardHeader",
            "children": [{"type": "CardTitle", "content": "Power Level"}]
          },
          {
            "type": "CardContent",
            "children": [
              {
                "cssClass": "gap-6",
                "type": "Column",
                "children": [
                  {
                    "name": "level",
                    "value": 50.0,
                    "type": "Slider",
                    "min": 0.0,
                    "max": 100.0,
                    "disabled": false,
                    "size": "default"
                  },
                  {
                    "cssClass": "gap-6 items-center",
                    "type": "Row",
                    "children": [
                      {
                        "type": "Ring",
                        "value": "{{ level }}",
                        "min": 0,
                        "max": 100,
                        "label": "{{ level }}%",
                        "variant": "{{ level > 75 ? 'destructive' : (level > 40 ? 'warning' : 'success') }}",
                        "size": "lg",
                        "thickness": 10.0
                      },
                      {
                        "cssClass": "gap-4 grid-cols-2 flex-1",
                        "type": "Grid",
                        "children": [
                          {
                            "cssClass": "gap-1",
                            "type": "Column",
                            "children": [
                              {"content": "Primary", "type": "Muted"},
                              {
                                "type": "Progress",
                                "value": "{{ level }}",
                                "max": 100.0,
                                "variant": "{{ level > 75 ? 'destructive' : (level > 40 ? 'warning' : 'success') }}",
                                "size": "default"
                              }
                            ]
                          },
                          {
                            "cssClass": "gap-1",
                            "type": "Column",
                            "children": [
                              {"content": "Inverse", "type": "Muted"},
                              {
                                "type": "Progress",
                                "value": "{{ 100 - level }}",
                                "max": 100.0,
                                "variant": "info",
                                "size": "default"
                              }
                            ]
                          },
                          {
                            "cssClass": "gap-1",
                            "type": "Column",
                            "children": [
                              {"content": "Doubled", "type": "Muted"},
                              {
                                "type": "Progress",
                                "value": "{{ level * 2 }}",
                                "max": 100.0,
                                "variant": "default",
                                "size": "default"
                              }
                            ]
                          },
                          {
                            "cssClass": "gap-1",
                            "type": "Column",
                            "children": [
                              {"content": "Halved", "type": "Muted"},
                              {
                                "type": "Progress",
                                "value": "{{ level / 2 }}",
                                "max": 100.0,
                                "variant": "success",
                                "size": "default"
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  },
                  {
                    "cssClass": "text-sm text-muted-foreground text-center",
                    "content": "Level is {{ level }}% \u2014 {{ level > 75 ? 'critical!' : (level > 40 ? 'nominal' : 'low') }}",
                    "type": "Text"
                  }
                ]
              }
            ]
          }
        ]
      },
      "state": {"level": 50.0}
    }
    ```
  </CodeGroup>
</ComponentPreview>

## How It Works

The entire page is driven by a single piece of [state](/concepts/state): `level`, a number from 0 to 100. The `Slider` declares `name="level"`, which binds it to that key — dragging the slider writes the new value back to state, and every component reading `level` re-renders. Nothing else holds state; the ring, the four bars, and the status line are all derived views of that one number.

`level = Rx("level")` creates a reactive reference to the state key. The power of [`Rx`](/concepts/expressions) is that ordinary Python operators on it build expressions rather than computing values immediately. `100 - level`, `level * 2`, and `level / 2` each compile to an expression string (`{{ 100 - level }}`, `{{ level * 2 }}`, `{{ level / 2 }}`) that the renderer evaluates live. That is how the "Inverse," "Doubled," and "Halved" bars stay in sync with the slider without any extra state or callbacks — they are arithmetic on the same source value.

The shared `variant` shows how to compute a value once and reuse it. `(level > 75).then("destructive", (level > 40).then("warning", "success"))` builds a nested ternary: above 75 it resolves to `destructive`, above 40 to `warning`, otherwise `success`. Assigning it to a Python variable lets the ring and the "Primary" progress bar both reference the same compiled expression, so their colors always agree as you cross each threshold.

Text interpolation works the same way. The `Ring` label `f"{level}%"` and the status line `f"Level is {level}% — ..."` embed reactive references inside f-strings, and an inline `.then()` chain inside the status f-string swaps the words `critical!`, `nominal`, and `low` at the same boundaries. Because every binding traces back to `level`, a single drag updates the whole card in one pass.
