API reference
Every exported helper, type, and macro. The 100-plus HTML tag constructors (div, h1, form, …) are generated programmatically and have no individual docstrings, so they aren't listed here — they come into scope with using HyperSignal (except the Base-shadowed div/select/summary/mark/time, which need @using_tags); see the Quickstart under Module overview below. Docstrings live alongside the source — this page just indexes them so the rendered docs site has clickable cross-references.
Module overview
HyperSignal — Module
HyperSignalDatastar-flavored HTML for Julia. Compose pages from typed AST nodes, render to streamed HTML, and bind Datastar actions without ever hand-typing data-on:click="@post('/x', {…})" or escaping HTML by hand.
Quickstart
using HyperSignal
HyperSignal.@using_tags # brings div / select / summary
# Build with tag constructors. Children go positional, attributes go kw.
page = Frag(
DOCTYPE,
html(lang="en",
head(meta(charset="UTF-8"), title("My App")),
body(
h1("Hello"),
form(on_submit(ds_post("/save"; form=true)),
radio_field("size", "S", "Small"),
radio_field("size", "L", "Large"; checked=true),
button(type="submit", "Save")),
)),
)
html_response(page) # full-page Response
fragment_response(page, "#card") # Datastar morph with selector headerWhat's exported
- AST primitives:
Element,Raw,Frag,Attribute,DOCTYPE. - Tag constructors: every common HTML element (
div,h1,form, …). Names that overlap with Base (div,select,summary) are brought into scope with the@using_tagsmacro. - Datastar actions:
ds_get,ds_post,ds_put,ds_delete, bound viaon/on_click/on_submit/on_change_debounced/on_interval.on(...)accepts raw JS expressions alongsideDSActionand awindow=truemodifier for global listeners. - Datastar attributes:
ds_indicator,ds_ignore_morph,ds_bind,ds_signal,ds_signals,ds_show,ds_text,ds_json_signals,ds_ref,ds_attr,ds_class,ds_computed,ds_style,ds_effect,ds_init. - Datastar signal decoding:
parse_signals(read the JSON body of a non-form Datastar action into aDict{String, Any}). - Form helpers:
cls,radio_field,checkbox_field,form_legend,form_section,help_tooltip,preset_button. - Dialog helper:
signal_dialog(native<dialog>driven by a Datastar expression). - Rendering:
render(io, x)for streaming,render(x)for the String you usually want at the response boundary. - Responses:
html_response,fragment_response,redirect_via_fragment,redirect_to.
Safety model
Strings and numbers in children / attribute values are auto-escaped at render time. Use Raw at the boundary to inject pre-built HTML (SVG snippets, audited generators) — never wrap user input in Raw. JS-string interpolation inside Datastar actions is the renderer's job; build a DSAction and let it through.
Element tree
HyperSignal.Element — Type
Element(tag::Symbol, attrs::Vector{Pair{Symbol,Any}}, children::Vector{Any})The HTML AST node. You almost never build one directly — call a tag constructor (div, h1, form, …) and let it split positional args / kwargs / Attribute values for you. Construct manually only if you're building a custom element with a non-static tag name.
Examples
# Direct construction — for a component with a tag chosen at runtime:
heading(level::Int, text) = Element(Symbol("h", level), Pair{Symbol,Any}[], Any[text])
heading(2, "Hello") # ≡ h2("Hello")Boolean-attribute policy
true renders the attribute as bare; false, nothing, and missing omit it entirely; any other value renders as a quoted, escaped string. Prior art (JuliaWeb/Hyperscript.jl#20) shows this corner is easy to get wrong — the doctest below pins it.
julia> render(input(type="checkbox", checked=true))
"<input type=\"checkbox\" checked>"
julia> render(input(type="checkbox", checked=false))
"<input type=\"checkbox\">"
julia> render(input(type="checkbox", checked=nothing))
"<input type=\"checkbox\">"
julia> render(input(type="checkbox", checked=missing))
"<input type=\"checkbox\">"
julia> render(input(type="checkbox", checked="yes"))
"<input type=\"checkbox\" checked=\"yes\">"HyperSignal.Frag — Type
Frag(children...)A "group of children with no wrapper tag". Use it to return multiple sibling elements from a single function, or to prepend DOCTYPE to an html(...) tree.
Examples
# A component that renders to two siblings — without an extra wrapper div:
section_with_grid(label, cards...) = Frag(
small(class="muted form-section-label", label),
div(class="form-card-grid", cards...),
)HyperSignal.Raw — Type
Raw(html::String)Trusted HTML that bypasses auto-escape. Wrap the value at the boundary — SVG icon strings, output of an audited HTML generator, third-party widget markup — never wrap user input.
Examples
const SPINNER = Raw("<svg viewBox="0 0 24 24">…</svg>")
div(class="loading", SPINNER, " Working…") # SVG kept verbatim, " Working…" escapedAdversarial round-trip
Raw performs zero rewriting — what you put in is what the renderer writes out. A regression that re-escapes Raw (or worse, sanitizes it) would break SVG icons and audited generators and fail this doctest. Never wrap user input.
julia> render(p(Raw("<img src=x onerror=alert(1)>")))
"<p><img src=x onerror=alert(1)></p>"HyperSignal.Attribute — Type
Attribute(key::Symbol, value)Attribute value returned by helpers like on and ds_indicator. Tag constructors filter these out of positional args and merge them into the attrs list, so an Attribute-returning helper drops in next to children without a splat ceremony.
You rarely construct Attribute directly — use the helpers. It's exported mostly so user code can pattern-match or filter on it.
Examples
button("Submit",
ds_indicator(), # Attribute
on(:click, ds_post("/api/submit")), # Attribute
" ", strong("now")) # childrenHyperSignal.DOCTYPE — Constant
DOCTYPEThe <!DOCTYPE html> prelude as a Raw constant. Drop it as the first child of a Frag wrapping html(...) so page builders don't hand-type the doctype string at every call site.
Examples
page = Frag(
DOCTYPE,
html(lang="en",
head(meta(charset="UTF-8"), title("My App")),
body(h1("Hello"))),
)
html_response(page)Rendering
HyperSignal.render — Function
render(x) -> StringRender x (any renderable: Element, Frag, Raw, strings, numbers, vectors, nothing/missing) into a String. Sibling of the streaming render(io, x) — same dispatch, just returns the bytes instead of writing them. Use this when you need a String for an HTTP body or a test assertion; reach for render(io, x) when you already hold an IO.
Examples
julia> using HyperSignal: div
julia> render(div(class="card", h2("Hi"), p("hello")))
"<div class=\"card\"><h2>Hi</h2><p>hello</p></div>"
julia> render("a < b & \"c\"")
"a < b & "c""
julia> render(nothing)
""HTTP response wrappers
HyperSignal.html_response — Function
html_response(body; status=200, headers=[]) -> HTTP.ResponseRender body (anything renderable) and wrap it in an HTTP.Response with Content-Type: text/html; charset=utf-8. Use this for full-page GETs.
Examples
julia> r = html_response(p("ok"));
julia> r.status
200
julia> Dict(r.headers)["Content-Type"]
"text/html; charset=utf-8"
julia> String(r.body)
"<p>ok</p>"
julia> r2 = html_response(p("created"); status=201, headers=["X-Tag" => "v1"]);
julia> r2.status, Dict(r2.headers)["X-Tag"]
(201, "v1")HyperSignal.fragment_response — Function
fragment_response(body; selector=nothing, mode=nothing,
view_transition=false, status=200, headers=[]) -> HTTP.Response
fragment_response(body, selector::AbstractString; kwargs...) -> HTTP.ResponseLike html_response but also surfaces the Datastar fragment control headers — datastar-selector (morph target), datastar-mode (swap mode), and datastar-use-view-transition — so a single helper covers any handler that swaps a fragment of an existing page.
selector— CSS selector for the morph target. Omit for whole-body morph.mode::Union{Nothing,Symbol}— one of:outer :inner :replace :prepend :append :before :after :remove.nothing(the default) omits the header so the Datastar client uses its default (outer). An unknown symbol throwsArgumentError.view_transition::Bool— whentrue, addsdatastar-use-view-transition: trueso the client wraps the swap in a View Transition.
The positional fragment_response(body, selector) form is preserved.
Examples
julia> r = fragment_response(p("ok"), "#count");
julia> r.status, Dict(r.headers)["datastar-selector"]
(200, "#count")
julia> String(r.body)
"<p>ok</p>"
julia> r2 = fragment_response(p("ok"); selector="#count", mode=:inner,
view_transition=true);
julia> Dict(r2.headers)["datastar-mode"], Dict(r2.headers)["datastar-use-view-transition"]
("inner", "true")HyperSignal.signals_response — Function
signals_response(signals; only_if_missing=false, status=200, headers=[]) -> HTTP.ResponseSend a Datastar JSON-signals patch. Body is JSON.json(signals) — pass anything JSON.jl knows how to encode (NamedTuple, Dict, struct). only_if_missing=true adds the datastar-only-if-missing: true header, which tells the client to skip the merge for any signal already on the page.
Examples
julia> r = signals_response((; count=3));
julia> r.status, Dict(r.headers)["Content-Type"]
(200, "application/json; charset=utf-8")
julia> String(r.body)
"{\"count\":3}"HyperSignal.script_response — Function
script_response(js::AbstractString; script_attributes=nothing,
status=200, headers=[]) -> HTTP.ResponseSend a Datastar text/javascript response — the client appends a <script> tag with js as its body and runs it. The body is written verbatim; the caller owns the escape. Never interpolate unsanitized user input.
script_attributes becomes the datastar-script-attributes header: an AbstractString passes through; anything else is JSON-encoded with JSON.json.
Examples
julia> r = script_response("alert('hi')");
julia> r.status, Dict(r.headers)["Content-Type"]
(200, "text/javascript; charset=utf-8")
julia> String(r.body)
"alert('hi')"HyperSignal.sse_response — Function
sse_response(events; status=200, headers=[]) -> HTTP.ResponseBuffer one or more Datastar SSE events (built by patch_elements / patch_signals) into a single text/event-stream response. Use this when a handler must emit an HTML patch and a signal patch in one shot.
Examples
julia> r = sse_response([
patch_elements(HyperSignal.div(id="card", "Saved"); selector="#card"),
patch_signals((; saved=true)),
]);
julia> r.status, Dict(r.headers)["Content-Type"]
(200, "text/event-stream; charset=utf-8")
julia> print(String(r.body))
event: datastar-patch-elements
data: selector #card
data: elements <div id="card">Saved</div>
event: datastar-patch-signals
data: signals {"saved":true}
HyperSignal.sse_stream — Function
sse_stream(f; status=200, headers=[]) -> stream handlerBuild an HTTP.jl stream handler that streams Datastar SSE events over a chunked text/event-stream response. f receives a writer callable: each call with a patch_elements / patch_signals event encodes the event and flushes it as its own chunk so the client sees progress in real time. Register the returned handler with HTTP.serve(handler, host, port; stream=true).
writer is not concurrency-safe — concurrent calls from multiple tasks will interleave chunks. Serialize calls (or guard writer with a ReentrantLock) if f fans out work.
Example
HTTP.serve(sse_stream() do writer
for i in 1:5
writer(patch_elements(div(id="progress", "step $i"); selector="#progress", mode=:inner))
sleep(0.5)
end
end, "127.0.0.1", 8080; stream=true)HyperSignal.patch_elements — Function
patch_elements(body; selector=nothing, mode=nothing, view_transition=false)Build a datastar-patch-elements SSE event for sse_response. body is rendered with render; multi-line HTML is split into one data: elements … line per source line. mode is one of the fragment modes accepted by fragment_response; unknown symbols throw ArgumentError.
HyperSignal.patch_signals — Function
patch_signals(signals; only_if_missing=false)Build a datastar-patch-signals SSE event. signals is JSON-encoded with JSON.json. only_if_missing=true adds the onlyIfMissing true data line so the client skips signals already present.
HyperSignal.redirect_via_fragment — Function
redirect_via_fragment(selector, location; cookies=String[], wrapper_tag=:div) -> HTTP.ResponseDatastar can't issue an HTTP 303 from a form submit it owns — the morph algorithm replaces the target instead. This helper wraps a tiny <script>window.location='…'</script> in the morph target so a Datastar form can navigate after success. Single quotes, backslashes, and </ sequences in location are escaped (the last to keep the HTML parser from closing the surrounding <script> tag mid-string).
Pass cookies as a vector of complete Set-Cookie header values to attach session cookies to the redirect — useful for the post-login flow where you need to set the cookie AND navigate in the same response. Use wrapper_tag when the morph target is something other than a <div> (e.g. :li for a <li> morph target).
For non-Datastar redirects (login form POST, plain navigation), use redirect_to instead.
Examples
# Login flow: morph #login-form to a navigation script + set session cookie
return redirect_via_fragment("#login-form", "/dashboard";
cookies=["sid=$token; HttpOnly; Path=/; SameSite=Lax"])HyperSignal.redirect_to — Function
redirect_to(location::AbstractString; cookies=String[]) -> HTTP.ResponsePlain HTTP 303 redirect for non-Datastar flows (login form POST, logout, direct navigation). Pass cookies as a vector of complete Set-Cookie header values to attach session cookies to the redirect — useful for the post-login flow where you want to redirect AND set the session cookie in the same response.
For Datastar form submits that need to navigate after success, use redirect_via_fragment instead — Datastar's morph algorithm won't follow a 303.
Examples
julia> r = redirect_to("/dashboard");
julia> r.status
303
julia> Dict(r.headers)["Location"]
"/dashboard"
julia> r2 = redirect_to("/home";
cookies=["sid=abc; HttpOnly; Path=/"]);
julia> [v for (k, v) in r2.headers if k == "Set-Cookie"]
1-element Vector{SubString{String}}:
"sid=abc; HttpOnly; Path=/"Version pinning
HyperSignal.DATASTAR_SUPPORTED_VERSION — Constant
DATASTAR_SUPPORTED_VERSIONThe Datastar protocol/client version HyperSignal is built and tested against. Pin your served datastar.js to this version; bumps land as one visible diff.
Datastar actions
HyperSignal.DSAction — Type
DSAction(verb, url, form, extras)A "Datastar request action" — verb + URL + options. Build via ds_get, ds_post, ds_put, or ds_delete; pass to on (or its on_* shorthands) to bind it to a DOM event. The renderer formats the JS expression at the attribute boundary so the verb/URL/options live in one place.
You rarely construct one directly — use the verb constructors.
HyperSignal.ds_get — Function
ds_get(url; form=false, kwargs...)Build a @get('url', {…}) Datastar action. Pass form=true to add contentType: 'form' (Datastar will encode form fields as application/x-www-form-urlencoded). Any further kwargs become {k: v} entries on the JS options object.
Examples
julia> HyperSignal.action_js(ds_get("/api/refresh"))
"@get('/api/refresh')"
julia> HyperSignal.action_js(ds_get("/api/session/count"; form=true))
"@get('/api/session/count', {contentType: 'form'})"HyperSignal.ds_post — Function
ds_post(url; form=false, kwargs...)Build a @post('url', {…}) Datastar action. Pass form=true for the common case of submitting a form; the rendered attribute is then @post('url', {contentType: 'form'}). Pass to on (or on_submit / on_click) to bind to a DOM event.
Examples
julia> HyperSignal.action_js(ds_post("/api/like"))
"@post('/api/like')"
julia> HyperSignal.action_js(ds_post("/session/new"; form=true))
"@post('/session/new', {contentType: 'form'})"HyperSignal.ds_put — Function
ds_put(url; form=false, kwargs...)Build a @put('url', {…}) Datastar action. See ds_post.
HyperSignal.ds_delete — Function
ds_delete(url; form=false, kwargs...)Build a @delete('url', {…}) Datastar action. See ds_post.
HyperSignal.on — Function
on(event::Symbol, action; debounce=nothing, window=false) -> AttributeBind a value to a DOM event. action is either a DSAction (the renderer formats it as @verb('url', {…})) or an AbstractString (a raw JS expression — useful for client-side toggles like "$open = !$open"). Returns an Attribute you drop into a tag's positional args.
Modifiers:
debounce=N(ms) — appends__debounce.Nms. Use for change events on inputs that should ignore mid-word typing.window=true— appends__window. Routes the listener towindowinstead of the element, so global hotkeys reach it without focus.prevent=true— appends__prevent. Callsevent.preventDefault()before runningaction. Defaults totruefor:submitso a form bound to a Datastar action doesn't also trigger the native navigation; passprevent=falseto opt out.stop=true— appends__stop. Callsevent.stopPropagation().outside=true— appends__outside. Routes the listener todocumentand only fires when the event target is NOT inside the bound element (the click-outside-to-close pattern).
Examples
julia> on(:click, ds_post("/api/x"; form=true)).key
Symbol("data-on:click")
julia> on(:submit, ds_post("/save")).key # auto __prevent on :submit
Symbol("data-on:submit__prevent")
julia> on(:submit, ds_post("/save"); prevent=false).key # opt-out
Symbol("data-on:submit")
julia> on(:change, ds_get("/c"); debounce=300).key
Symbol("data-on:change__debounce.300ms")
julia> on(:keydown, "$open = true"; window=true).key
Symbol("data-on:keydown__window")HyperSignal.on_click — Function
on_click(action; debounce=nothing)
on_submit(action; debounce=nothing)Single-event shorthands for on(:click, action) / on(:submit, action). Read better than on(:click, …) in component bodies that bind exactly one event.
Examples
button("Dismiss", on_click(ds_post("/api/dismiss")))
form(on_submit(ds_post("/save"; form=true)), …)HyperSignal.on_submit — Function
on_click(action; debounce=nothing)
on_submit(action; debounce=nothing)Single-event shorthands for on(:click, action) / on(:submit, action). Read better than on(:click, …) in component bodies that bind exactly one event.
Examples
button("Dismiss", on_click(ds_post("/api/dismiss")))
form(on_submit(ds_post("/save"; form=true)), …)HyperSignal.on_change_debounced — Function
on_change_debounced(action; ms=300) -> AttributeShorthand for on(:change, action; debounce=ms). The default 300ms is the cadence used across this codebase for form-driven live updates — short enough to feel instant, long enough to ignore mid-word typing.
Examples
form(on_change_debounced(ds_get("/api/preview"; form=true)),
input(type="text", name="query"))HyperSignal.on_interval — Function
on_interval(action; ms=5000) -> AttributeRun action (a DSAction or raw JS expression) on a recurring interval. Renders as data-on-interval__duration.Nms="…". The default 5-second cadence matches the dashboard-stats polling pattern in this codebase.
data-on-interval is a Datastar plugin distinct from data-on:event — it doesn't take an event name, only a duration modifier.
Examples
section(id="dataset-stats",
on_interval(ds_get("/api/dashboard/stats"); ms=5000),
…)Datastar attributes
HyperSignal.ds_indicator — Function
ds_indicator() -> AttributeMark an element as a Datastar request indicator. The element becomes visible while a Datastar action initiated under it is in flight, and hides again on completion — Datastar adds/removes the visibility via the data-indicator attribute the renderer emits.
Examples
button("Save", on_click(ds_post("/api/save")),
span(class="spinner", ds_indicator(), "…"))ds_indicator(signal::AbstractString) -> AttributeMark an element as the indicator for a named in-flight signal. Datastar sets signal to true while requests under this scope are in flight, so sibling elements can ds_show("$signal") a spinner or grey out a panel without each having to track the request lifecycle themselves.
HyperSignal.ds_ignore_morph — Function
ds_ignore_morph() -> AttributeTell Datastar's morph algorithm to leave this element's subtree alone across fragment swaps. Useful for inputs the user is currently typing in or focused elements you don't want re-rendered.
Examples
input(type="text", name="search", ds_ignore_morph())HyperSignal.ds_bind — Function
ds_bind(signal::AbstractString) -> AttributeTwo-way bind an input to a Datastar signal: the input's value mirrors signal, and edits flow back. Returns the data-bind="signal" attribute.
Examples
input(type="text", ds_bind("query"))HyperSignal.ds_signal — Function
ds_signal(name::AbstractString, value) -> AttributeInitialize a Datastar signal on this element. Renders as the keyed data-signals:<name>="value" form. Note Datastar's kebab→camel mapping: ds_signal("my-signal", …) creates the signal $mySignal.
Examples
div(ds_signal("count", 0), ds_text("count")) # signal "count" starts at 0HyperSignal.ds_signals — Function
ds_signals(state) -> AttributeInitialize a whole Datastar signals object on this element. state is anything JSON-encodable — typically a NamedTuple or Dict of signal name → initial value. Renders as data-signals='{...}' after attribute escape (the JSON's " round-trip cleanly through ").
Use this in place of ds_signal when one element seeds several signals at once (e.g. a card with showDetails + confirmDialogOpen + …); the JSON encoding catches the kinds of typos that hand-written {"x": false, "y": false} strings drop into client-side silence.
Examples
julia> a = ds_signals((showDetails=false, count=0));
julia> a.value
"{\"showDetails\":false,\"count\":0}"
julia> a.key
Symbol("data-signals")HyperSignal.ds_show — Function
ds_show(expr::AbstractString) -> AttributeShow this element only when the JS expression expr is truthy. Renders as data-show="expr".
Examples
p(ds_show("count > 0"), "You have items.")HyperSignal.ds_text — Function
ds_text(expr::AbstractString) -> AttributeSet this element's text content from the JS expression expr. Renders as data-text="expr". Use this instead of templating a value into a string when the value is a Datastar signal that may change client-side.
Examples
span(ds_text("count")) # text content tracks the signal "count"HyperSignal.ds_json_signals — Function
ds_json_signals() -> Attribute
ds_json_signals(filter::AbstractString) -> AttributeSet this element's text content to a live, JSON-stringified view of the Datastar signal store — the standard in-page signal debugger. Drop a pre(ds_json_signals()) onto a page during development and it tracks the store reactively as signals change. Renders as the bare data-json-signals attribute (no value).
Pass filter — a Datastar filter-object JS expression such as "{include: /user/}" or "{exclude: /temp$/}" — to scope the output to matching signal names. Renders as data-json-signals="<filter>".
Examples
julia> a = ds_json_signals();
julia> (a.key, a.value)
(Symbol("data-json-signals"), true)
julia> b = ds_json_signals("{include: /user/}");
julia> b.value
"{include: /user/}"HyperSignal.ds_ref — Function
ds_ref(name::AbstractString) -> AttributeMark this element with a Datastar ref so other Datastar expressions can reach it as $<name> (e.g. $btnNext.click()). Renders as data-ref="name".
Examples
button(ds_ref("btnNext"), "Next")
# Elsewhere: data-on:keydown__window="if(event.key==='ArrowRight') $btnNext.click()"HyperSignal.ds_attr — Function
ds_attr(name::AbstractString, expr::AbstractString) -> AttributeReactively bind a DOM attribute to a Datastar expression: as expr changes (because a signal it reads changes), the attribute updates. Renders as data-attr:NAME="expr". Truthy → attribute set; falsy → attribute removed.
Examples
# Open/close a <dialog> from a signal
dialog(ds_attr("open", "$dialogOpen"), …)
# Disable a button while a request is in flight
button(ds_attr("disabled", "$saving"), "Save")HyperSignal.ds_class — Function
ds_class(name::AbstractString, expr::AbstractString) -> AttributeToggle a CSS class reactively. Renders as data-class:NAME="expr" — when expr evaluates truthy Datastar adds the class, when falsy it removes it. Pair-style sibling of ds_attr; use ds_class when the target is a class on class=, ds_attr when the target is any other attribute.
Examples
# Drop the `.outline` class from the active view-toggle button
button(class="grid-toggle", ds_class("outline", "$view !== 'grid'"), "Grid")HyperSignal.ds_computed — Function
ds_computed(name::AbstractString, expr::AbstractString) -> AttributeDeclare a read-only derived signal computed from a Datastar expression. The computed signal name re-evaluates whenever any signal expr reads changes. Renders as data-computed:NAME="expr". Reach it elsewhere as $NAME (totals, validation flags, formatted strings). Use this instead of recomputing the same expression at every read site.
Datastar camel-cases hyphenated signal names, so ds_computed("full-name", …) is read as $fullName; prefer camelCase names to avoid surprise.
Examples
# A line-item total that tracks its inputs; read elsewhere as $total
div(ds_computed("total", "$price * $qty"),
span(ds_text("$total")))HyperSignal.ds_style — Function
ds_style(name::AbstractString, expr::AbstractString) -> AttributeSet an inline CSS style property reactively. Renders as data-style:NAME="expr" — Datastar evaluates expr and writes the result to element.style.NAME, keeping it in sync as the signals it reads change. The reactive-binding sibling of ds_class (toggle a class) and ds_attr (bind any attribute); reach for ds_style when the value is a dynamic dimension/color/transform that isn't expressible as a static class.
Examples
# Drive a progress bar's width from a signal (0–100)
div(class="bar", ds_style("width", "$pct + '%'"))
# Hide via inline display rather than a class
div(ds_style("display", "$hiding && 'none'"))HyperSignal.ds_effect — Function
ds_effect(expr::AbstractString) -> AttributeRun a Datastar JS effect: a side-effecting expression that re-evaluates whenever the signals it reads change. Useful for "imperative bridge" moments where you have to call a DOM method from signal state (e.g. $dialog.showModal()). Renders as data-effect="expr".
Examples
# Open or close a dialog as `$dialogOpen` changes
div(ds_effect("$dialogOpen ? $dlg.showModal() : $dlg.close()"))HyperSignal.ds_init — Function
ds_init(action_or_expr) -> AttributeRun an action or JS expression once when this element is inserted into the DOM. Pass a DSAction for an HTTP fetch, or an AbstractString for a raw JS expression. Renders as data-init="…".
Examples
# Fetch the first card on element insert
div(id="card-container",
ds_init(ds_get("/api/review/card?session_id=$(id)")),
…)
# Or initialise a signal from a JS computation
div(ds_signals((width=0,)), ds_init("$width = window.innerWidth"))Signal decoding
HyperSignal.parse_signals — Function
parse_signals(req_or_body) -> Dict{String, Any}Decode the Datastar signals payload from a request body. Datastar's default action mode (@post('/x') without contentType: 'form') sends the active signals object as a JSON body. Pass either an HTTP.Request, a Vector{UInt8}, or an AbstractString — the helper normalizes the input and returns the parsed object as a Dict{String, Any}. Empty bodies map to an empty dict so a route can guard cleanly.
For form-encoded posts (@post('/x', {contentType: 'form'})), use the service's parse_form_body instead — Datastar treats form-mode and JSON-mode as distinct wire formats, and so does this lib.
Examples
function handle_increment(req::HTTP.Request)
sig = parse_signals(req)
n = Int(get(sig, "count", 0)) + 1
fragment_response(div(id="counter", n), "#counter")
endComponent helpers
Top-level — primitives that don't pull in an app idiom:
HyperSignal.cls — Function
cls(parts...) -> StringBuild a CSS class attribute string from a flexible mix of inputs. Accepts:
AbstractString→ kept as-is (empty strings drop)."name" => bool→ included only when the bool is true.Vectorof any of the above → flattened.nothing/missing→ skipped.
Empty inputs collapse to "" so a class=cls(...) attribute is safe even when nothing matches. Pair values that aren't Bool raise a loud error — a typo like "active" => "yes" would otherwise silently include the class.
Examples
julia> cls("btn", "primary", "active" => true)
"btn primary active"
julia> cls("btn", "primary", "active" => false)
"btn primary"
julia> cls("btn", ["large", "rounded"], "loading" => false)
"btn large rounded"
julia> cls()
""HyperSignal.Helpers
App-grade building blocks. Pull them in with using HyperSignal.Helpers: … — there is no top-level export and no deprecation shim.
HyperSignal.Helpers.radio_field — Function
radio_field(name::AbstractString, value::AbstractString, text::AbstractString; checked=false)Render a <label><input type="radio" name=… value=… [checked]> text</label> — the label-around-input convention. One call replaces ~6 lines of hand-built input-and-label boilerplate per choice.
Examples
julia> render(radio_field("size", "S", "Small"))
"<label><input type=\"radio\" name=\"size\" value=\"S\"> Small</label>"
julia> render(radio_field("size", "L", "Large"; checked=true))
"<label><input type=\"radio\" name=\"size\" value=\"L\" checked> Large</label>"HyperSignal.Helpers.checkbox_field — Function
checkbox_field(name::AbstractString, text::AbstractString; checked=false, value="on")Render a <label><input type="checkbox" name=… value=… [checked]> text</label>. The default value="on" matches the form-encoded shape parse_form_body (and any standard form parser) expects — keep the default unless your backend explicitly wants a different value.
Examples
julia> render(checkbox_field("notify_email", "Email"))
"<label><input type=\"checkbox\" name=\"notify_email\" value=\"on\"> Email</label>"
julia> render(checkbox_field("agree", "I agree"; checked=true))
"<label><input type=\"checkbox\" name=\"agree\" value=\"on\" checked> I agree</label>"
julia> render(checkbox_field("opt", "Opt-in"; value="yes"))
"<label><input type=\"checkbox\" name=\"opt\" value=\"yes\"> Opt-in</label>"HyperSignal.Helpers.text_field — Function
text_field(label_text::AbstractString, name::AbstractString;
type="text", required=false)Render a <label for=name>text</label><input type=… id=name name=name [required]> pair as a Frag. The for/id attribute pair ties them so the input keeps focus when the label is clicked, and screen readers announce the label whether or not the input is the label's child.
Defaults to type="text"; pass type="password" for a masked input. Pass required=true to mark the field as a constraint the browser enforces before allowing submit.
Examples
form(on_submit(ds_post("/login"; form=true)),
text_field("Username", "username"; required=true),
text_field("Password", "password"; type="password", required=true),
button(type="submit", "Log in"))HyperSignal.Helpers.form_legend — Function
form_legend(text::AbstractString; tooltip=nothing)Render <legend class="muted">text [help-tooltip]</legend>. Pass tooltip to attach an inline help icon via help_tooltip. Without a tooltip the result is just a plain muted legend.
Examples
julia> render(form_legend("Size"))
"<legend class=\"muted\">Size</legend>"For the with-tooltip variant the output includes a hashed id that isn't byte-stable across the tooltip text, so see the help_tooltip docstring for the structural details.
HyperSignal.Helpers.form_section — Function
form_section(label_text::AbstractString, cards...)Wrap a list of "card" elements (typically <article>s) under a muted section header. Renders a Frag of <small class="muted form-section-label">label</small> and <div class="form-card-grid">cards…</div> — collapses the section-header + grid pattern that opens every section in this codebase's session form.
Returns a Frag (no wrapper element), so it inlines into a <form> without forcing an extra <div> you'd then have to style around.
Examples
form_section("Image Batch",
article(fieldset(form_legend("Size"), radio_field("n", "10", "10"))),
article(fieldset(form_legend("Source"), radio_field("src", "a", "A"))),
)HyperSignal.Helpers.help_tooltip — Function
help_tooltip(text::AbstractString; icon=DEFAULT_HELP_ICON)Render
<span class="help-trigger" tabindex="0"
data-signals="{help_open: '', help_hover: ''}"
data-on:mouseenter="$help_hover = '<id>'"
data-on:mouseleave="$help_hover = ''"
data-on:click__outside="$help_open === '<id>' && ($help_open = '')">
<span class="help-icon-wrap"
data-on:click="$help_open = $help_open === '<id>' ? '' : '<id>'">
[icon]
</span>
<span class="help-popup" role="tooltip"
data-show="$help_open === '<id>' || $help_hover === '<id>'">text</span>
</span>The popup opens on hover (and closes on mouseleave) and toggles on click of the icon, staying open until the user clicks anywhere outside the trigger (datastar's __outside modifier on a document-level listener). id is a stable hash of the tooltip text so two helpers with the same copy share state — fine, since they'd say the same thing.
Tooltip text is auto-escaped so caller copy can include quotes / < / & without worry. Override icon with your own Raw/Element for projects with a different help glyph.
Most code reaches for form_legend instead — it pairs a legend with this tooltip in one call.
Examples
legend("Confidence ", help_tooltip("ML certainty range. Lower = ambiguous."))HyperSignal.Helpers.preset_button — Function
preset_button(text::AbstractString, settings::AbstractVector{<:Pair{<:AbstractString,<:AbstractString}})Render a "preset" button: clicking it sets each named radio input to checked (matching value), then dispatches a bubbling change event on the form so any data-on:change handler (e.g. a live-count GET) recomputes. The <button onclick="…"> JS is built once here so each preset doesn't repeat the escape-prone querySelector boilerplate.
settings is a vector of name => value pairs identifying the radios to flip.
Examples
fieldset(
form_legend("Quick presets"),
preset_button("Easy", ["confidence" => "all", "label_filter" => "both"]),
preset_button("Hard", ["confidence" => "hard", "label_filter" => "iw"]),
)HyperSignal.Helpers.signal_dialog — Function
signal_dialog(open_expr, body...; close_action, id=nothing, class="")Render a <dialog> whose open/close state is mirrored to a Datastar expression. Collapses the boilerplate of pairing ds_effect("$x ? $dlg.showModal() : $dlg.close()") with a hand-rolled backdrop div and gives every dialog the same close semantics:
data-effectreadsopen_expr; truthy →el.showModal()(puts the dialog in the top layer with native focus trap, ESC, and::backdrop), falsy →el.close(). Datastar exposes the host element aselinside expressions;thisis the signals proxy, not the DOM node.data-on:closerunsclose_actionwhenever the dialog closes by any means (ESC, programmatic, formmethod=dialog) so the bound signal stays in sync without the caller threading it through every dismiss site.data-on:clickchecksevent.target === eland runsclose_action— that's the standard "click the backdrop area to close" affordance. Inner content must be wrapped in a child element so its clicks don't match (a top-level child<div>/<article>is enough).
close_action is a JS statement (no trailing semicolon needed) that restores the signal to its closed state — typically "$modal = 0" or "$confirmOpen = false". The same statement runs from both the :close listener (ESC, programmatic) and the backdrop click, so the signal converges to false/0 no matter how the user dismissed.
Examples
# Page-level lightbox indexed by an integer signal
signal_dialog("$lightbox",
div(class="lightbox-frame",
# Each panel ds_show-gated by the signal value
(panel(i) for i in 1:n)...);
close_action="$lightbox = 0", class="image-lightbox")
# Boolean-driven confirm dialog
signal_dialog("$confirmOpen",
article(header(strong("Confirm")), p("Commit?"),
button(on_click(ds_post("/api/commit")), "Yes"),
button(on_click("$confirmOpen = false"), "No"));
close_action="$confirmOpen = false", id="confirm-commit-dialog")SVG inlining
Pure string transforms — no Makie dependency. patch_svg / inline_svg take any SVG string (CairoMakie's save("plot.svg", fig) output is the motivating case). A Figure / Scene / FigureAxisPlot overload of inline_svg is added by the HyperSignalMakieExt extension when any Makie backend (e.g. CairoMakie) is loaded.
HyperSignal.patch_svg — Function
patch_svg(svg::AbstractString;
id_prefix::AbstractString = "",
strip_size::Bool = true,
add_class::Union{AbstractString, Nothing} = nothing,
aria_label::Union{AbstractString, Nothing} = nothing) -> StringRewrite an SVG document string so it can be inlined into HTML without breaking. Returns the patched SVG as a String — wrap with Raw to drop straight into a HyperSignal tree, or use inline_svg to do both in one call.
Transforms applied (each independently controlled):
- The XML prolog (
<?xml …?>) and any<!DOCTYPE …>are removed; both are invalid inside HTML and would otherwise trip the parser. HTML comments (<!-- … -->) are stripped too — anywhere in the document, not just the prolog — since backends like CairoMakie emit generator notes that add bytes without affecting the rendered figure. - When
strip_size=true, the root<svg>'swidthandheightattributes are removed, leaving onlyviewBoxso the figure scales to its CSS container. CairoMakie hard-codes px dimensions; strip them unless the page really wants the original size. - When
id_prefixis non-empty, everyid="…",url(#…), andxlink:href="#…"/href="#…"is rewritten with the prefix. This is the only safe way to inline more than one CairoMakie figure on the same page — itsclip0/glyph0ids collide otherwise. - When
add_classis set, the value is appended to the root<svg>'sclassattribute (or a newclass="…"is added). - When
aria_labelis set,role="img"andaria-label="…"are added to the root<svg>so screen readers announce the figure.
For accessibility, prefer passing aria_label over relying on surrounding text — the SVG is the figure, and screen readers traverse it in isolation.
Examples
julia> patch_svg("""<?xml version="1.0"?><svg width="800" height="600" viewBox="0 0 8 6"><g/></svg>""")
"<svg viewBox=\"0 0 8 6\"><g/></svg>"
julia> patch_svg("""<svg viewBox="0 0 1 1"><defs><clipPath id="c0"><rect/></clipPath></defs><g clip-path="url(#c0)"/></svg>""";
id_prefix="fig_")
"<svg viewBox=\"0 0 1 1\"><defs><clipPath id=\"fig_c0\"><rect/></clipPath></defs><g clip-path=\"url(#fig_c0)\"/></svg>"HyperSignal.inline_svg — Function
inline_svg(figure; kwargs...) -> RawRender a Makie/CairoMakie Figure / Scene / FigureAxisPlot to SVG and inline it. Requires using CairoMakie (or any backend that emits image/svg+xml) in the caller's session — without it, the method body isn't loaded and you get a MethodError.
Keyword arguments are forwarded to patch_svg.
Examples
using CairoMakie, HyperSignal
fig = Figure(); lines(fig[1, 1], 1:10, rand(10))
div(class="plot", inline_svg(fig; id_prefix="fig1_", aria_label="Random walk"))Macros
HyperSignal.@using_tags — Macro
@using_tagsBring the Base-shadowed tag constructors (div, select, summary, mark, time) into the current module's scope. Equivalent to the explicit using HyperSignal: div, select, summary, mark, time line — saves callers from memorizing which names conflict.
Plain using HyperSignal already imports every other tag (h1, form, button, …) automatically; only the Base-shadowed set needs this.
Examples
using HyperSignal
HyperSignal.@using_tags
div(class="card", select(name="kind", option("a"), option("b")))