Build a front end by writing a document

star-burxt turns a .sbmx file into a working component — one that renders, responds to clicks, holds state, fetches, and routes.

A component is markup, and the logic that goes with it, in one file:

::: props count: Int
:::

# Counter

The count is {{ to_string(count) }}.

::: button on:click=count + 1
increment
:::

You get this:

The counter document on the left, and on the right the same document running in a browser, reading 'The count is 3.' with increment and reset buttons
The right-hand side is that document, running. It has been clicked three times.

No build config. No component library. No JavaScript to write. The document is the component.

today.example
A task list rendered by star-burxt: a
           heading, a count, and five rows with checkboxes, two of them completed.
Hero.sbmx the markup half
{% raw %}
::: section class=card
# Today
::: p
{{ to_string(model.left) }} left
:::
::: for task in model.tasks key to_string(task.id)
::: button class={{ mark(task.done) }} on:click=Msg.Toggle(string_to_int(key, 0))
::: span class=box
:::
::: span class=text
{{ task.label }}
:::
:::
:::
:::

That screenshot is the real component, running as WebAssembly — clicking a row runs your update. The other half of the file is Burxt: a Model, a Msg, and one update function, which chapter 7 walks through.

For anything past a counter, a component has two halves — the markup, and a ===bx section holding your own Burxt:

===bx
class Item  { id: Int, name: String }
class Model { count: Int, items: [Item] }
enum Msg { Increment, Reset }

pure function update(msg: Msg, m: Model) -> Model {
    match msg {
        Increment => { return Model { count: m.count + 1, items: m.items }; }
        Reset     => { return Model { count: 0, items: m.items }; }
    }
}
===

::: props model: Model
:::

# Counter

The count is {{ to_string(model.count) }}.

::: button on:click=Msg.Increment
increment
:::

on:click=Msg.Increment names a message, and your update decides what it does. That’s one function you can read, test, and diff — not a closure that re-runs.

What a component can do

You write You get
# Heading and paragraphs headings and paragraphs, as in any Markdown
{{ total }} your data, in the page
::: props model: Model what the component is given
::: button on:click=Msg.Add a button, and your update decides what it does
::: for item in items key item.id a row per item, each with its own buttons
::: match model.route a screen per route — forget one and the build fails
::: if ready / ::: else a section that appears when it should
::: Badge amount={{ n }} another component, imported with use
===style.local CSS scoped to this component, with no default
commands and watch fetching, timers, websockets — without await
load server rendering, and server code that cannot reach the browser

That is the whole idea. If you can write a document, you can write an app.

Start with the tour

Your first component →

Six short chapters. By the end you will have built a counter, a form, a list, and a price table, and put them on a real page.

The one thing that will surprise you

star-burxt reads your buttons.

Most frameworks treat what happens on a click as your business — it is a function, it runs, and if it is wrong you find out when a user finds out. Here, a click is checked before the page exists.

Misspell something:

The total is {{ toatl }}
unknown variable: toatl

Try to add a word to a number:

::: button on:click=count + "one"
increment
:::
cannot apply `+` to Int and String

Round money without saying how:

::: button on:click=total * 1.5
apply surcharge
:::
A receipt document on the left, and on the right the checker refusing the multiplication in its on:click handler
That last one is the interesting case, and it has its own chapter.

You do not have to know why this works to use it. If you would like to, Burxt is the language underneath and the explanation lives there.

Where to go