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

# Todo List

> A fully interactive todo list built with client-side state actions.

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>;
};

Multiple todo lists with inline editing, validation, and dynamic groups — all running client-side with no server calls.

<ComponentPreview json={{"view":{"cssClass":"pf-app-root","type":"Div","children":[{"cssClass":"gap-6 items-start","type":"Grid","minColumnWidth":"12rem","children":[{"let":{"_loop_11":"{{ $item }}","_loop_11_idx":"{{ $index }}"},"type":"ForEach","key":"groups","children":[{"type":"Card","children":[{"type":"CardHeader","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"cssClass":"border-0 ring-0 shadow-none p-0 h-auto font-semibold text-lg focus-visible:ring-0 focus-visible:ring-offset-0","name":"groups.{{ _loop_11_idx }}.name","type":"Input","inputType":"text","disabled":false,"readOnly":false,"required":false},{"cssClass":"ml-auto","type":"Button","label":"\u00d7","variant":"ghost","size":"sm","disabled":false,"onClick":{"action":"popState","key":"groups","index":"{{ _loop_11_idx }}"}}]}]},{"type":"CardContent","children":[{"cssClass":"gap-4","type":"Column","children":[{"cssClass":"gap-0","type":"Form","onSubmit":[{"action":"appendState","key":"groups.{{ _loop_11_idx }}.todos","value":{"text":"{{ _loop_11.new_todo }}","done":false}},{"action":"setState","key":"groups.{{ _loop_11_idx }}.new_todo","value":""}],"children":[{"cssClass":"gap-2","type":"Row","children":[{"name":"groups.{{ _loop_11_idx }}.new_todo","type":"Input","inputType":"text","placeholder":"Add a todo...","disabled":false,"readOnly":false,"required":false},{"type":"Button","label":"Add","variant":"default","size":"default","disabled":"{{ !_loop_11.new_todo }}"}]}]},{"cssClass":"gap-2","type":"Column","children":[{"type":"Condition","cases":[{"when":"{{ _loop_11.todos | length }}","children":[{"cssClass":"my-3","type":"Separator","orientation":"horizontal"}]}]},{"let":{"_loop_12":"{{ $item }}","_loop_12_idx":"{{ $index }}"},"type":"ForEach","key":"groups.{{ _loop_11_idx }}.todos","children":[{"type":"Condition","cases":[{"when":"{{ !_loop_12.done || _loop_11.show_done }}","children":[{"cssClass":"gap-2 items-center","type":"Row","children":[{"name":"groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.done","value":false,"type":"Checkbox","disabled":false,"required":false},{"cssClass":"border-0 ring-0 shadow-none p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0","name":"groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.text","type":"Input","inputType":"text","disabled":false,"readOnly":false,"required":false},{"cssClass":"ml-auto","type":"Button","label":"\u00d7","variant":"ghost","size":"sm","disabled":false,"onClick":{"action":"popState","key":"groups.{{ _loop_11_idx }}.todos","index":"{{ _loop_12_idx }}"}}]}]}]}]}]}]}]},{"type":"CardFooter","children":[{"cssClass":"gap-2 items-center w-full justify-between","type":"Row","children":[{"content":"{{ _loop_11.todos | length }} items","type":"Muted"},{"cssClass":"gap-1 items-center","type":"Row","children":[{"cssClass":"h-3 w-3","name":"groups.{{ _loop_11_idx }}.show_done","value":false,"type":"Checkbox","disabled":false,"required":false},{"content":"Show done","type":"Muted"}]}]}]}]}]},{"cssClass":"border-dashed p-3","type":"Card","children":[{"cssClass":"text-4xl text-muted-foreground w-full h-full min-h-48 rounded-md","type":"Button","label":"+","variant":"ghost","size":"lg","disabled":false,"onClick":{"action":"appendState","key":"groups","value":{"name":"New List","todos":[],"new_todo":"","show_done":true}}}]}]}]},"state":{"groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.done":false,"groups.{{ _loop_11_idx }}.show_done":false,"groups":[{"name":"Work","todos":[{"text":"Find Magrathea","done":false},{"text":"Escape Vogons","done":false}],"new_todo":"","show_done":true},{"name":"Personal","todos":[{"text":"Have lunch at Milliways","done":true},{"text":"Buy towel","done":false}],"new_todo":"","show_done":true}]}}} playground="ZnJvbSBwcmVmYWJfdWkuYXBwIGltcG9ydCBQcmVmYWJBcHAKZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cyBpbXBvcnQgKAogICAgQnV0dG9uLCBDYXJkLCBDYXJkQ29udGVudCwgQ2FyZEZvb3RlciwgQ2FyZEhlYWRlciwKICAgIENoZWNrYm94LCBDb2x1bW4sIEZvcm0sIEdyaWQsIElucHV0LCBNdXRlZCwgUm93LCBTZXBhcmF0b3IsCikKZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cy5jb250cm9sX2Zsb3cgaW1wb3J0IEZvckVhY2gsIElmCmZyb20gcHJlZmFiX3VpLmFjdGlvbnMgaW1wb3J0IEFwcGVuZFN0YXRlLCBQb3BTdGF0ZSwgU2V0U3RhdGUKZnJvbSBwcmVmYWJfdWkucnggaW1wb3J0IFJ4CgojIEVhY2ggZ3JvdXAgaGFzIGEgbmFtZSwgYSBsaXN0IG9mIHRvZG9zLCBhbiBpbnB1dCBidWZmZXIsIGFuZCBhIHZpc2liaWxpdHkgZmxhZwp3aXRoIEdyaWQobWluX2NvbHVtbl93aWR0aD0iMTJyZW0iLCBnYXA9NiwgYWxpZ249InN0YXJ0IikgYXMgdmlldzoKCiAgICAjIERlc3RydWN0dXJlIHRvIGNhcHR1cmUgdGhlIGdyb3VwIGluZGV4IChnaSkgYW5kIHRoZSBncm91cCBpdHNlbGYuCiAgICAjIEVhY2ggRm9yRWFjaCBhdXRvLWNhcHR1cmVzICRpdGVtIGFuZCAkaW5kZXggaW50byBzY29wZWQgbGV0IGJpbmRpbmdzLAogICAgIyBzbyBuZXN0ZWQgbG9vcHMgZG9uJ3Qgc2hhZG93IGVhY2ggb3RoZXIuCiAgICB3aXRoIEZvckVhY2goImdyb3VwcyIpIGFzIChnaSwgZ3JvdXApOgogICAgICAgIHdpdGggQ2FyZCgpOgogICAgICAgICAgICB3aXRoIENhcmRIZWFkZXIoKToKICAgICAgICAgICAgICAgIHdpdGggUm93KGdhcD0yLCBhbGlnbj0iY2VudGVyIik6CiAgICAgICAgICAgICAgICAgICAgSW5wdXQoCiAgICAgICAgICAgICAgICAgICAgICAgIG5hbWU9ZiJncm91cHMue2dpfS5uYW1lIiwKICAgICAgICAgICAgICAgICAgICAgICAgY3NzX2NsYXNzPSJib3JkZXItMCByaW5nLTAgc2hhZG93LW5vbmUgcC0wIGgtYXV0byBmb250LXNlbWlib2xkIHRleHQtbGcgZm9jdXMtdmlzaWJsZTpyaW5nLTAgZm9jdXMtdmlzaWJsZTpyaW5nLW9mZnNldC0wIiwKICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgQnV0dG9uKCLDlyIsIHZhcmlhbnQ9Imdob3N0Iiwgc2l6ZT0ic20iLCBjc3NfY2xhc3M9Im1sLWF1dG8iLCBvbl9jbGljaz1Qb3BTdGF0ZSgiZ3JvdXBzIiwgZ2kpKQogICAgICAgICAgICB3aXRoIENhcmRDb250ZW50KCk6CiAgICAgICAgICAgICAgICB3aXRoIENvbHVtbihnYXA9NCk6CiAgICAgICAgICAgICAgICAgICAgd2l0aCBGb3JtKGdhcD0wLCBvbl9zdWJtaXQ9WwogICAgICAgICAgICAgICAgICAgICAgICBBcHBlbmRTdGF0ZShmImdyb3Vwcy57Z2l9LnRvZG9zIiwgeyJ0ZXh0IjogZ3JvdXAubmV3X3RvZG8sICJkb25lIjogRmFsc2V9KSwKICAgICAgICAgICAgICAgICAgICAgICAgU2V0U3RhdGUoZiJncm91cHMue2dpfS5uZXdfdG9kbyIsICIiKSwKICAgICAgICAgICAgICAgICAgICBdKToKICAgICAgICAgICAgICAgICAgICAgICAgd2l0aCBSb3coZ2FwPTIpOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgSW5wdXQobmFtZT1mImdyb3Vwcy57Z2l9Lm5ld190b2RvIiwgcGxhY2Vob2xkZXI9IkFkZCBhIHRvZG8uLi4iKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgQnV0dG9uKCJBZGQiLCBkaXNhYmxlZD1-Z3JvdXAubmV3X3RvZG8pCgogICAgICAgICAgICAgICAgICAgIHdpdGggQ29sdW1uKGdhcD0yKToKICAgICAgICAgICAgICAgICAgICAgICAgd2l0aCBJZihncm91cC50b2Rvcy5sZW5ndGgoKSk6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBTZXBhcmF0b3Ioc3BhY2luZz0zKQogICAgICAgICAgICAgICAgICAgICAgICB3aXRoIEZvckVhY2goZiJncm91cHMue2dpfS50b2RvcyIpIGFzICh0aSwgdG9kbyk6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB3aXRoIElmKH50b2RvLmRvbmUgfCBncm91cC5zaG93X2RvbmUpOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdpdGggUm93KGdhcD0yLCBhbGlnbj0iY2VudGVyIik6CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIENoZWNrYm94KG5hbWU9ZiJncm91cHMue2dpfS50b2Rvcy57dGl9LmRvbmUiKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBJbnB1dCgKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5hbWU9ZiJncm91cHMue2dpfS50b2Rvcy57dGl9LnRleHQiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY3NzX2NsYXNzPSJib3JkZXItMCByaW5nLTAgc2hhZG93LW5vbmUgcC0wIGgtYXV0byBmb2N1cy12aXNpYmxlOnJpbmctMCBmb2N1cy12aXNpYmxlOnJpbmctb2Zmc2V0LTAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEJ1dHRvbigiw5ciLCB2YXJpYW50PSJnaG9zdCIsIHNpemU9InNtIiwgY3NzX2NsYXNzPSJtbC1hdXRvIiwgb25fY2xpY2s9UG9wU3RhdGUoZiJncm91cHMue2dpfS50b2RvcyIsIHRpKSkKCiAgICAgICAgICAgIHdpdGggQ2FyZEZvb3RlcigpOgogICAgICAgICAgICAgICAgd2l0aCBSb3coZ2FwPTIsIGFsaWduPSJjZW50ZXIiLCBjc3NfY2xhc3M9InctZnVsbCBqdXN0aWZ5LWJldHdlZW4iKToKICAgICAgICAgICAgICAgICAgICBNdXRlZChmIntncm91cC50b2Rvcy5sZW5ndGgoKX0gaXRlbXMiKQogICAgICAgICAgICAgICAgICAgIHdpdGggUm93KGdhcD0xLCBhbGlnbj0iY2VudGVyIik6CiAgICAgICAgICAgICAgICAgICAgICAgIENoZWNrYm94KG5hbWU9ZiJncm91cHMue2dpfS5zaG93X2RvbmUiLCBjc3NfY2xhc3M9ImgtMyB3LTMiKQogICAgICAgICAgICAgICAgICAgICAgICBNdXRlZCgiU2hvdyBkb25lIikKCiAgICB3aXRoIENhcmQoY3NzX2NsYXNzPSJib3JkZXItZGFzaGVkIHAtMyIpOgogICAgICAgIEJ1dHRvbigKICAgICAgICAgICAgIisiLAogICAgICAgICAgICB2YXJpYW50PSJnaG9zdCIsCiAgICAgICAgICAgIHNpemU9ImxnIiwKICAgICAgICAgICAgY3NzX2NsYXNzPSJ0ZXh0LTR4bCB0ZXh0LW11dGVkLWZvcmVncm91bmQgdy1mdWxsIGgtZnVsbCBtaW4taC00OCByb3VuZGVkLW1kIiwKICAgICAgICAgICAgb25fY2xpY2s9QXBwZW5kU3RhdGUoImdyb3VwcyIsIHsibmFtZSI6ICJOZXcgTGlzdCIsICJ0b2RvcyI6IFtdLCAibmV3X3RvZG8iOiAiIiwgInNob3dfZG9uZSI6IFRydWV9KSwKICAgICAgICApCgphcHAgPSBQcmVmYWJBcHAoCiAgICBzdGF0ZT17CiAgICAgICAgImdyb3VwcyI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICAgIm5hbWUiOiAiV29yayIsCiAgICAgICAgICAgICAgICAidG9kb3MiOiBbCiAgICAgICAgICAgICAgICAgICAgeyJ0ZXh0IjogIkZpbmQgTWFncmF0aGVhIiwgImRvbmUiOiBGYWxzZX0sCiAgICAgICAgICAgICAgICAgICAgeyJ0ZXh0IjogIkVzY2FwZSBWb2dvbnMiLCAiZG9uZSI6IEZhbHNlfSwKICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAibmV3X3RvZG8iOiAiIiwKICAgICAgICAgICAgICAgICJzaG93X2RvbmUiOiBUcnVlLAogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAibmFtZSI6ICJQZXJzb25hbCIsCiAgICAgICAgICAgICAgICAidG9kb3MiOiBbCiAgICAgICAgICAgICAgICAgICAgeyJ0ZXh0IjogIkhhdmUgbHVuY2ggYXQgTWlsbGl3YXlzIiwgImRvbmUiOiBUcnVlfSwKICAgICAgICAgICAgICAgICAgICB7InRleHQiOiAiQnV5IHRvd2VsIiwgImRvbmUiOiBGYWxzZX0sCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5ld190b2RvIjogIiIsCiAgICAgICAgICAgICAgICAic2hvd19kb25lIjogVHJ1ZSwKICAgICAgICAgICAgfSwKICAgICAgICBdLAogICAgfSwKICAgIHZpZXc9dmlldywKKQo">
  <CodeGroup>
    ```python Python icon="python" expandable theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.app import PrefabApp
    from prefab_ui.components import (
        Button, Card, CardContent, CardFooter, CardHeader,
        Checkbox, Column, Form, Grid, Input, Muted, Row, Separator,
    )
    from prefab_ui.components.control_flow import ForEach, If
    from prefab_ui.actions import AppendState, PopState, SetState
    from prefab_ui.rx import Rx

    # Each group has a name, a list of todos, an input buffer, and a visibility flag
    with Grid(min_column_width="12rem", gap=6, align="start") as view:

        # Destructure to capture the group index (gi) and the group itself.
        # Each ForEach auto-captures $item and $index into scoped let bindings,
        # so nested loops don't shadow each other.
        with ForEach("groups") as (gi, group):
            with Card():
                with CardHeader():
                    with Row(gap=2, align="center"):
                        Input(
                            name=f"groups.{gi}.name",
                            css_class="border-0 ring-0 shadow-none p-0 h-auto font-semibold text-lg focus-visible:ring-0 focus-visible:ring-offset-0",
                        )
                        Button("×", variant="ghost", size="sm", css_class="ml-auto", on_click=PopState("groups", gi))
                with CardContent():
                    with Column(gap=4):
                        with Form(gap=0, on_submit=[
                            AppendState(f"groups.{gi}.todos", {"text": group.new_todo, "done": False}),
                            SetState(f"groups.{gi}.new_todo", ""),
                        ]):
                            with Row(gap=2):
                                Input(name=f"groups.{gi}.new_todo", placeholder="Add a todo...")
                                Button("Add", disabled=~group.new_todo)

                        with Column(gap=2):
                            with If(group.todos.length()):
                                Separator(spacing=3)
                            with ForEach(f"groups.{gi}.todos") as (ti, todo):
                                with If(~todo.done | group.show_done):
                                    with Row(gap=2, align="center"):
                                        Checkbox(name=f"groups.{gi}.todos.{ti}.done")
                                        Input(
                                            name=f"groups.{gi}.todos.{ti}.text",
                                            css_class="border-0 ring-0 shadow-none p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0",
                                        )
                                        Button("×", variant="ghost", size="sm", css_class="ml-auto", on_click=PopState(f"groups.{gi}.todos", ti))

                with CardFooter():
                    with Row(gap=2, align="center", css_class="w-full justify-between"):
                        Muted(f"{group.todos.length()} items")
                        with Row(gap=1, align="center"):
                            Checkbox(name=f"groups.{gi}.show_done", css_class="h-3 w-3")
                            Muted("Show done")

        with Card(css_class="border-dashed p-3"):
            Button(
                "+",
                variant="ghost",
                size="lg",
                css_class="text-4xl text-muted-foreground w-full h-full min-h-48 rounded-md",
                on_click=AppendState("groups", {"name": "New List", "todos": [], "new_todo": "", "show_done": True}),
            )

    app = PrefabApp(
        state={
            "groups": [
                {
                    "name": "Work",
                    "todos": [
                        {"text": "Find Magrathea", "done": False},
                        {"text": "Escape Vogons", "done": False},
                    ],
                    "new_todo": "",
                    "show_done": True,
                },
                {
                    "name": "Personal",
                    "todos": [
                        {"text": "Have lunch at Milliways", "done": True},
                        {"text": "Buy towel", "done": False},
                    ],
                    "new_todo": "",
                    "show_done": True,
                },
            ],
        },
        view=view,
    )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "cssClass": "pf-app-root",
        "type": "Div",
        "children": [
          {
            "cssClass": "gap-6 items-start",
            "type": "Grid",
            "minColumnWidth": "12rem",
            "children": [
              {
                "let": {"_loop_11": "{{ $item }}", "_loop_11_idx": "{{ $index }}"},
                "type": "ForEach",
                "key": "groups",
                "children": [
                  {
                    "type": "Card",
                    "children": [
                      {
                        "type": "CardHeader",
                        "children": [
                          {
                            "cssClass": "gap-2 items-center",
                            "type": "Row",
                            "children": [
                              {
                                "cssClass": "border-0 ring-0 shadow-none p-0 h-auto font-semibold text-lg focus-visible:ring-0 focus-visible:ring-offset-0",
                                "name": "groups.{{ _loop_11_idx }}.name",
                                "type": "Input",
                                "inputType": "text",
                                "disabled": false,
                                "readOnly": false,
                                "required": false
                              },
                              {
                                "cssClass": "ml-auto",
                                "type": "Button",
                                "label": "\u00d7",
                                "variant": "ghost",
                                "size": "sm",
                                "disabled": false,
                                "onClick": {"action": "popState", "key": "groups", "index": "{{ _loop_11_idx }}"}
                              }
                            ]
                          }
                        ]
                      },
                      {
                        "type": "CardContent",
                        "children": [
                          {
                            "cssClass": "gap-4",
                            "type": "Column",
                            "children": [
                              {
                                "cssClass": "gap-0",
                                "type": "Form",
                                "onSubmit": [
                                  {
                                    "action": "appendState",
                                    "key": "groups.{{ _loop_11_idx }}.todos",
                                    "value": {"text": "{{ _loop_11.new_todo }}", "done": false}
                                  },
                                  {"action": "setState", "key": "groups.{{ _loop_11_idx }}.new_todo", "value": ""}
                                ],
                                "children": [
                                  {
                                    "cssClass": "gap-2",
                                    "type": "Row",
                                    "children": [
                                      {
                                        "name": "groups.{{ _loop_11_idx }}.new_todo",
                                        "type": "Input",
                                        "inputType": "text",
                                        "placeholder": "Add a todo...",
                                        "disabled": false,
                                        "readOnly": false,
                                        "required": false
                                      },
                                      {
                                        "type": "Button",
                                        "label": "Add",
                                        "variant": "default",
                                        "size": "default",
                                        "disabled": "{{ !_loop_11.new_todo }}"
                                      }
                                    ]
                                  }
                                ]
                              },
                              {
                                "cssClass": "gap-2",
                                "type": "Column",
                                "children": [
                                  {
                                    "type": "Condition",
                                    "cases": [
                                      {
                                        "when": "{{ _loop_11.todos | length }}",
                                        "children": [{"cssClass": "my-3", "type": "Separator", "orientation": "horizontal"}]
                                      }
                                    ]
                                  },
                                  {
                                    "let": {"_loop_12": "{{ $item }}", "_loop_12_idx": "{{ $index }}"},
                                    "type": "ForEach",
                                    "key": "groups.{{ _loop_11_idx }}.todos",
                                    "children": [
                                      {
                                        "type": "Condition",
                                        "cases": [
                                          {
                                            "when": "{{ !_loop_12.done || _loop_11.show_done }}",
                                            "children": [
                                              {
                                                "cssClass": "gap-2 items-center",
                                                "type": "Row",
                                                "children": [
                                                  {
                                                    "name": "groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.done",
                                                    "value": false,
                                                    "type": "Checkbox",
                                                    "disabled": false,
                                                    "required": false
                                                  },
                                                  {
                                                    "cssClass": "border-0 ring-0 shadow-none p-0 h-auto focus-visible:ring-0 focus-visible:ring-offset-0",
                                                    "name": "groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.text",
                                                    "type": "Input",
                                                    "inputType": "text",
                                                    "disabled": false,
                                                    "readOnly": false,
                                                    "required": false
                                                  },
                                                  {
                                                    "cssClass": "ml-auto",
                                                    "type": "Button",
                                                    "label": "\u00d7",
                                                    "variant": "ghost",
                                                    "size": "sm",
                                                    "disabled": false,
                                                    "onClick": {
                                                      "action": "popState",
                                                      "key": "groups.{{ _loop_11_idx }}.todos",
                                                      "index": "{{ _loop_12_idx }}"
                                                    }
                                                  }
                                                ]
                                              }
                                            ]
                                          }
                                        ]
                                      }
                                    ]
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      },
                      {
                        "type": "CardFooter",
                        "children": [
                          {
                            "cssClass": "gap-2 items-center w-full justify-between",
                            "type": "Row",
                            "children": [
                              {"content": "{{ _loop_11.todos | length }} items", "type": "Muted"},
                              {
                                "cssClass": "gap-1 items-center",
                                "type": "Row",
                                "children": [
                                  {
                                    "cssClass": "h-3 w-3",
                                    "name": "groups.{{ _loop_11_idx }}.show_done",
                                    "value": false,
                                    "type": "Checkbox",
                                    "disabled": false,
                                    "required": false
                                  },
                                  {"content": "Show done", "type": "Muted"}
                                ]
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              },
              {
                "cssClass": "border-dashed p-3",
                "type": "Card",
                "children": [
                  {
                    "cssClass": "text-4xl text-muted-foreground w-full h-full min-h-48 rounded-md",
                    "type": "Button",
                    "label": "+",
                    "variant": "ghost",
                    "size": "lg",
                    "disabled": false,
                    "onClick": {
                      "action": "appendState",
                      "key": "groups",
                      "value": {"name": "New List", "todos": [], "new_todo": "", "show_done": true}
                    }
                  }
                ]
              }
            ]
          }
        ]
      },
      "state": {
        "groups.{{ _loop_11_idx }}.todos.{{ _loop_12_idx }}.done": false,
        "groups.{{ _loop_11_idx }}.show_done": false,
        "groups": [
          {
            "name": "Work",
            "todos": [
              {"text": "Find Magrathea", "done": false},
              {"text": "Escape Vogons", "done": false}
            ],
            "new_todo": "",
            "show_done": true
          },
          {
            "name": "Personal",
            "todos": [
              {"text": "Have lunch at Milliways", "done": true},
              {"text": "Buy towel", "done": false}
            ],
            "new_todo": "",
            "show_done": true
          }
        ]
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

## How It Works

The state is an array of groups, each containing a `name`, a `todos` array, and a `new_todo` input buffer. [ForEach](/docs/components/foreach) iterates the groups, and a nested ForEach iterates each group's todos.

**Nested loops** — `with ForEach("groups") as (gi, group)` destructures the loop binding into an index (`gi`) and item (`group`), matching Python's `enumerate` convention. Each ForEach automatically captures `$item` and `$index` into scoped `let` bindings, so the outer loop's `group` and `gi` survive even after the inner loop shadows `$item` and `$index`. The inner `ForEach(f"groups.{gi}.todos") as (ti, todo)` uses `gi` to target the right group and `ti` to target individual todos.

**Inline editing** — instead of showing todo text as a `Text` component, each item uses a borderless `Input` styled with `border-0 shadow-none p-0` so it looks like plain text but is directly editable. [Dot-path auto-binding](/docs/actions/update-state#dot-paths) handles the read and write — the input's `name` points at the exact item in state (`f"groups.{gi}.todos.{ti}.text"`).

**Disabling the Add button** — `disabled=~group.new_todo` evaluates to true when the input is empty, so the button stays disabled until you type something. `group` is the outer ForEach's item, so `group.new_todo` accesses the group's input buffer.

**Hiding completed items** — each group has a `show_done` flag toggled by a small checkbox in the footer. Inside the inner todo loop, `If(~todo.done | group.show_done)` checks the todo's completion against the group's visibility flag. Both references survive nesting because ForEach's auto-bind captured them.

**Adding a group** — the dashed card at the end appends a new group object with `AppendState("groups", {...})`. ForEach picks it up and renders a new card automatically.
