Skip to content

Inputs & controls

Controls are how a frame becomes interactive. They come in two flavours, and the difference is where their value goes:

  • Filter controls@block/filter_bar, @block/search, @block/field_filter, @block/facet, @block/query_bar — push a filter into the surrounding @block/filter_context, which descendant queries automatically respect. You wire nothing; drop a filter context around a table and a filter bar above it, and adding a filter filters the table.
  • State controls@block/switch, @block/checkbox, @block/option, @block/timerange — read and write a shared state key. Bind the control and a query to the same key (@expr/context_state / @expr/state) and the page reacts as the reader changes it.

@block/button triggers an action, @block/pagination / @block/cycle step through data, and @block/crosshair coordinates chart interaction. Full options are in the block reference.

@block/timerange and @block/query_bar

The time picker. On its own, @block/timerange drives the page’s shared _timerange, which every context-bound query already follows — so adding it makes the whole page time-aware. @block/query_bar is the common header combo: a filter bar and a time picker in one row — the default page header. It forwards filters and placeholder to the filter bar.

{ "@block/query_bar": {} }

@block/filter_bar

The filter surface. Displays the active filters from the surrounding filter context as pills — each with a funnel toggle (temporarily excludes the filter from queries without removing it), a clickable label that reopens the predicate editor to replace the filter in place, and a remove button — plus a plus button that expands an in-place, full-width predicate input with autocomplete. Edits commit only via the Done button or Enter; the predicate is first validated with a cheap database round-trip, and one that fails (bad syntax, unknown column, type error) cannot be committed — the database’s error is shown inline. size picks the control tier: sm (default, matches the time picker) or xs (dense). Filters are combined with AND. Predefine filters with filters; each takes an expression (a boolean SQL predicate), an optional label, and an optional disabled to start paused. On a multi-table view, set table to scope every filter the bar owns (and its autocomplete/validation) to one table; omit it to operate across all view tables (autocomplete unions their columns, and committed filters apply to every table).

{
"@block/filter_bar": {
"filters": [{ "expression": "StatusCode = 'Error'", "label": "Errors only" }]
}
}

Filters can also be added straight from the data, no bar interaction needed. Hovering a @block/table cell reveals a menu button beside its value that filters to or excludes that cell’s value. The actions toggle — applying one that’s already on takes it back off, and the menu reads “Remove filter” instead — so the same click both sets and clears. When the column is a view column (a with: scalar or a physical column of the view tables), it pushes a frame-wide filter — an ordinary filter-context pill that surfaces in a @block/filter_bar if one is present and re-queries the whole frame. Any other filterable column — most usefully a grouped table’s aggregate alias, which can’t fold frame-wide — filters that table locally instead, shown as a removable chip above its body. A categorical @block/plot legend item opens the same quick menu on click — alongside its local “only show this” / “hide this series” series toggles — when its coloured series resolves to a view column.

By default a @block/table column is filterable when it’s string-typed. A column’s filter prop overrides that: false opts out, true opts in regardless of type, and a string is a table-local predicate the cell value is injected into. A bare column name ("WorkspaceName") is shorthand for "WorkspaceName = ?"; ? binds the clicked cell’s value (positional) and {Column} binds that row’s value for another column (named), so "lower(name) = lower(?)" and "Region = {Region} AND Cost > ?" both work. Exclude negates the whole predicate.

@block/search, @block/field_filter, and @block/facet

Filtering controls. @block/search commits free text as a filter; @block/field_filter autocompletes a column’s values in a combobox and filters on the selection; @block/facet is the labeled sidebar variant — it lists a column’s values inline as a checkbox group, with a display limit and searchable: true to add a search input above the list for high-cardinality fields. That input’s placeholder belongs to the input, so it is authored inside searchable: "searchable": { "placeholder": "Find a service…" }. All write into the nearest @block/filter_context, so the recipe is: a filter context wrapping the data, with these controls inside it.

Scope a control to the view table that defines the column with table, so a multi-table view doesn’t inject the column into a query whose table lacks it. Omit table and the control spans every view table — @block/search then autocompletes the union of their columns (ones present in every table bubble to the top) and validates a committed predicate against each, since the filter folds into all of them.

{ "@block/field_filter": { "field": "StatusCode", "placeholder": "Filter by status…" } }
{ "@block/facet": { "field": "Model", "table": "metrics_sum" } }

@block/switch, @block/checkbox, @block/option

The bound-state controls — a segmented control, a checkbox, and a single/multi select. Reach for these when the reader’s choice should change a query or another block: bind the control’s value and the query to the same key.

@block/switch is a segmented control with one segment per case — the interactive mirror of @block/case. Its single top-level key is a shared state slot (declared in a surrounding @block/context); the case map turns each value into a labelled segment, and selecting one writes that value to the slot. A @block/case or a query bound to the same slot then reacts.

{
"@block/switch": {
"status": { "active": "Active", "inactive": "Inactive" }
}
}

Prefer @block/switch whenever the choice is a handful of peer values — roughly three or fewer — so every option stays visible and one click away.

@block/option is the single/multi select, and earns its dropdown when a segmented control is the wrong shape: many options, options resolved from data, multi-select, or a choice where one entry is an absence rather than a peer value. A placeholder plus a “None” entry expresses “not set” in a way a segmented control can’t. The arity tag — single or multiple — names the whole control, so everything it takes lives inside it.

{
"@block/option": {
"single": {
"placeholder": "Break down by",
"options": [{ "title": "None" }, { "title": "By host" }, { "title": "By region" }],
"selected": { "@expr/context_state": "breakdown" }
}
}
}

@block/button

Triggers an action on click — most often opening a @block/drawer or another triggerable. Use it for “view details”, “open settings”, anything that reveals more without navigating away.

{
"@block/button": {
"title": "Open",
"click": { "@block/drawer": { "title": "Details", "@block/text": "Drawer body" } }
}
}

@block/pagination and @block/cycle

Step through a dataset a slice at a time. @block/pagination writes the current page into a binding (drive a list or a @block/for from it); @block/cycle is the headless version that advances through items — useful for a rotating view. Both write the current slice/item into a state key you then read elsewhere.

{
"@block/pagination": {
"data": { "@expr/query": "SELECT * FROM events" },
"displayed": { "@expr/context_state": "displayed" },
"page_size": 25
}
}

@block/crosshair

Wraps a group of charts so they share a hover crosshair and tooltip, and lets the reader brush a time axis to set the page’s time range. Wrap your plots in one to make a row of charts feel like a single coordinated view. A drag commits every brushable axis at once, so a chart whose value axis declares a filter brush picks a range on both.

{ "@block/crosshair": { "top_n": 5, "block": { "@block/text": "Charts" } } }