LeCodesdocs

Checklist

Done-state lives in a per-task signal, and the row binds a style class to it with bindClass. Classes cascade: $done set on the button is active on all of its children too — the circle, the checkmark and the title each declare their own $done block with its own transition, so the row restyles in place, no remounts. The checkmark itself is a separate SVG file in the project: importing it yields an icon you recolor with tintColor.

Fork in LeCodes
// Rows never remount: done-state lives in a per-task signal and each row binds a style class to
// it. Classes cascade — `$done` set on the button is also active on all of its children — so the
// circle, the checkmark and the title each declare their own `$done` block, each with its own
// transition. The checkmark is an SVG file beside this one; importing it yields a tintable icon.
import check from "./check.svg"

type Task = { title: string, done: Signal<boolean> }

const tasks: Task[] = [
    { title: "Read the conventions", done: signal(true) },
    { title: "Fork an example", done: signal(false) },
    { title: "Run it on a phone", done: signal(false) },
    { title: "Ship something small", done: signal(false) },
]
const left = computed(() => tasks.filter(t => !t.done.value).length)

const Row = (task: Task) => UIButton([
    UIColumn([
        UIImage(check).style({ 
            width: 12, 
            height: 12, 
            tintColor: "white",
            opacity: 0, 
            $done: { opacity: 1, duration: 150 } 
        }),
    ]).style({ 
        width: 22, 
        height: 22, 
        borderRadius: 11, 
        border: "2px solid #39404f",
        justifyContent: "center", 
        alignItems: "center",
        $done: { bgColor: "#22c55e", borderColor: "#22c55e", duration: 150 } 
    }),
    UIText(task.title).style({ 
        fontSize: 16, 
        color: "white",
        $done: { color: "#5c6272", textDecoration: "line-through", duration: 150 } 
    }),
]).style({ height: 58, px: 16, gap: 14, borderRadius: 14, bgColor: "#151922",
    justifyContent: "flex-start",
    // $pressed is toggled by the system, and cascades exactly like a class of your own.
    $pressed: { transform: "scale(0.98)", duration: 100 } })
    .class({ done: () => task.done.value })
    .onClick(() => task.done.value = !task.done.value)

const screen = UIScreen([
    UIText("Today").style({ fontSize: 30, fontWeight: 700, color: "white" }),
    UIText(() => `${left.value} left`).style({ fontSize: 14, color: "#8b90a0", mb: 6 }),
    UIScrollable(tasks.map(Row)).style({ flexGrow: 1, gap: 8 }),
]).style({ bgColor: "#0b0d12", px: 16, pt: "max(safe-top, 24px)", pb: "comfort-bottom" })

screen.open()

Related docs