
Not all reactivity requires a server round-trip. Four attributes handle common UI patterns using only client-side signal state.
A computed signal is derived from an expression over other signals. It is read-only and updates automatically whenever its dependencies change. Declare it with data-computed:name on any element, then reference $name like any other signal.
(:strong :|data-computed:demoupper| "$demotext.toUpperCase()"
:data-text "$demoupper")Computed signals are useful for memoizing expressions that appear in multiple bindings, or for deriving a display form without a server call. Note: HTML attribute names are lowercased by the browser before Datastar reads them. Signal names that appear as attribute keys (after the colon) must be lowercase.
data-show toggles CSS display based on a Datastar expression. When the expression is false the element is hidden via display: none. Set style="display:none" as the initial inline style to prevent a flash of the element before Datastar processes the page.
(:p :data-show "$demotext != ''" :style "display:none"
"Only visible when input is non-empty")data-attr:name binds any HTML attribute to an expression. When the expression is truthy the attribute is set; when falsy it is removed. The attribute name is written in kebab-case after the colon.
(:button :|data-attr:disabled| "$demotext == ''"
:|data-attr:aria-disabled| "$demotext == ''"
"Save")This covers both functional disabling and ARIA accessibility in one expression. The Datastar docs call out data-attr:aria-* as the standard way to make reactive components accessible.
Type in the input below. No server is contacted. The computed uppercase preview appears when the input is non-empty; the Save button enables as soon as there is text.
data-indicator:name sets signal $name to true while a backend request is in flight, and clears it when the response arrives. Pair it with data-show to display a loading state:
(:button :|data-on:click| (d*:sse-get "/slow-endpoint")
:|data-indicator:demofetching| ""
"Fetch")
(:span :data-show "$demofetching" :style "display:none" "Loading...")The loading text disappears the moment the server sends its first event and Datastar clears $demofetching. This is the right pattern: show progress, wait for the server, let the response update the DOM.
Do not use optimistic updates. Updating the UI before the server confirms the action deceives the user: they see success, only to have it corrected a moment later if the server disagrees. A loading indicator is honest.
Click Fetch. The server sleeps 1.2 seconds before responding. The Loading indicator is visible during the wait and disappears when the result arrives.
A self-contained program covering both demos. Load with sbcl --load demo-reactivity.lisp and visit http://localhost:8989.
;;;; -*- Mode: LISP; fill-column: 80; coding: utf-8 -*-
;;;; DEMO-REACTIVITY.LISP --- Client-side reactive attributes and loading
;;;;
;;;; Copyright (C) 2025, 2026 Frederico Muñoz / ΛↃ lambda combine
;;;;
;;;; This file is part of datastar-cl, the Common Lisp SDK for Datastar
;;;;
;;;; License: MIT
;;; Client-side data-* attributes that need no server round-trip:
;;; data-computed, data-show, data-attr. Plus data-indicator with a slow
;;; endpoint to show the loading pattern while a request is in flight.
;;;
;;; Note: HTML attribute names are lowercased by the browser before Datastar
;;; reads them. Signal names that appear as attribute keys (after the colon,
;;; e.g. data-computed:name, data-indicator:name) must be lowercase.
;;;
;;; Run with: sbcl --load demo-reactivity.lisp
(ql:quickload '(:hunchentoot :spinneret :datastar-cl/hunchentoot))
(defpackage #:reactivity
(:use #:cl #:hunchentoot)
(:local-nicknames (:sp :spinneret) (:d* :datastar-cl)))
(in-package #:reactivity)
(hunchentoot:define-easy-handler (index :uri "/") ()
(setf (hunchentoot:content-type*) "text/html")
(sp:with-html-string
(:doctype)
(:html
(:head (:script :type "module" :src (d*:datastar-url)))
(:body :data-signals "{name: '', fetching: false}"
(:h1 "Datastar-CL Reactivity Demo")
;; Client-only: no server round-trip for any of these.
;;
;; data-bind "name" creates signal $name, two-way bound to input.
;; data-computed:upper creates read-only derived signal $upper.
;; data-show shows/hides the preview paragraph.
;; data-attr:disabled / data-attr:aria-disabled are reactive attributes.
(:h2 "Client reactivity")
(:input :data-bind "name" :placeholder "Type something")
(:p :data-show "$name != ''" :style "display:none"
"Preview: "
(:strong :|data-computed:upper| "$name.toUpperCase()"
:data-text "$upper"))
(:button :|data-attr:disabled| "$name == ''"
:|data-attr:aria-disabled| "$name == ''"
"Save")
(:hr)
;; Loading indicator: data-indicator sets $fetching while the
;; request is in flight. data-show uses it to toggle "Loading...".
;; The /slow endpoint sleeps before responding to make it visible.
(:h2 "Loading indicator")
(:button :|data-on:click| (d*:sse-get "/slow")
:|data-indicator:fetching| ""
"Fetch (slow)")
(:span :data-show "$fetching" :style "display:none" " Loading...")
(:span :id "result")))))
(hunchentoot:define-easy-handler (slow :uri "/slow") ()
(d*:with-sse (gen hunchentoot:*request*)
(sleep 1.2)
(multiple-value-bind (s m h) (decode-universal-time (get-universal-time))
(d*:patch-elements gen
(format nil "<span>Response at ~2,'0d:~2,'0d:~2,'0d</span>" h m s)
:selector "#result"
:mode :inner))))
(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 8989))
(format t "~&Server started on http://localhost:8989~%")
Remaining principles not covered by the previous eight chapters:
render-* helpers in chapters 06, 07, and 08 are the Common Lisp expression of this principle.<a> elements. For server-side redirects use redirect (chapter 03). Do not manage browser history yourself.datastar-cl/brotli (which appends :br to the priority list) and optionally reorder:(ql:quickload :datastar-cl/brotli) ; adds :br to *default-compression-priority*
;; Default after loading: (:zstd :br) -- prefer zstd, fall back to brotli.
;; To prefer brotli first:
(setf d*:*default-compression-priority* '(:br :zstd))(defroute guide-reactivity (:get :text/html))