Markup Project Guide

Comprehensive guide for @beforesemicolon/markup. Use this as a quick onboarding for humans and AI agents exploring the repo.

Contents

Overview

Markup is a tiny reactive templating system that keeps you close to web standards. It relies on JavaScript functions and tagged template literals to describe the DOM declaratively, then reconciles changes efficiently without a virtual DOM.

Quick Start

Install

bash
                npm install @beforesemicolon/markup
            

CDN (no build step)

html
                <script src="https://unpkg.com/@beforesemicolon/markup/dist/client.js"></script>
<script>
    const { html, state, effect } = BFS.MARKUP
    html`<h1>Hello World</h1>`.render(document.body)
</script>
            

Minimal counter

ts
                import { html, state, effect } from '@beforesemicolon/markup'

const [count, setCount] = state(0)
const increment = () => setCount((prev) => prev + 1)

effect(() => {
    if (count() > 10) alert('Passed 10!')
})

html`
    <p>Count: ${count}</p>
    <button type="button" onclick="${increment}">Count up</button>
`.render(document.getElementById('app'))
            

Core APIs

html – reactive templates

ts
                const [first] = state('Ada')
const [last] = state('Lovelace')

const fullName = () => `${first()} ${last()}` // function as data
html`<h1>Hello, ${fullName}</h1>` // do NOT call it here

const styles = () => `color: ${first() === 'Ada' ? 'teal' : 'black'}`
html`<p style="${styles}">${fullName}</p>` // template calls styles and fullName
            

state – reactive values

ts
                const [value, setValue, unsubscribe] = state(initial, optionalSubscriber)
setValue(next | (prev) => next)
unsubscribe() // stop the optional subscriber
            

Example (unsubscribe after one update):

ts
                const [value, setValue, stopLog] = state(0, () => {
    console.log('value changed to', value())
})

setValue(1) // logs once
stopLog() // detach the subscriber
setValue(2) // no log
            

effect – side effects

ts
                const removeEffect = effect(() => {
    console.log('value is', value())
    // return optional cleanup or derived value
})
// later: removeEffect()
            

Template Utilities (what/when/how)

These helpers are primarily for stateful data. For static values, plain JS (ternaries, map, literals) is fine.

when(condition, then, else?)

ts
                // Reactive: reruns when stateful isLoading changes
html`${when(isLoading, 'Loading…', 'Ready')}`

// Static: simple ternary is clearer
html`${isStatic ? 'Yes' : 'No'}`

// Booleans: bind directly instead of wrapping in when
html`<button disabled="${isSaving}">Save</button>`
            

Avoid: nesting complex logic inside when. Compute booleans first, e.g.:

ts
                const canSubmit = () => isAuth() && hasPlan()
html`${when(canSubmit, 'Go', 'Login required')}`
            

repeat(data, mapFn, whenEmpty?)

ts
                const [items] = state([{ id: 1, name: 'A' }]) // Array
html`${repeat(
    items,
    (item) => html`<li>${item.name}</li>`,
    () => 'No items'
)}`

const setData = new Set(['a', 'b']) // Set
html`${repeat(setData, (v) => html`<span>${v}</span>`)}`

const mapData = new Map([
    [1, 'one'],
    [2, 'two'],
]) // Map (receives [key,value])
html`${repeat(mapData, ([k, v]) => html`<p>${k}:${v}</p>`)}`

const count = 3 // Number (renders 1..n)
html`${repeat(count, (n, i) => html`<b>${n}</b>`)}`

const obj = { a: 1, b: 2 } // Object (entries)
html`${repeat(obj, ([k, v]) => html`<em>${k}:${v}</em>`)}`

const iterable = {
    // Custom iterable
    *[Symbol.iterator]() {
        yield 'x'
        yield 'y'
    },
}
html`${repeat(iterable, (v) => html`<i>${v}</i>`)}`
            

For static arrays, array.map inline is fine. Use repeat when the list is reactive so DOM nodes update efficiently. Avoid passing a non-iterable (will render empty/throw).

is(a, b) / isNot(a, b)

ts
                html`${when(is(status, 'error'), 'Retry', 'Submit')}`
html`${when(
    is(value, (v) => v > 10),
    'Big',
    'Small'
)}` // predicate
html`${when(is(flag), 'Truthy', 'Falsy')}` // no second arg: truthy/falsy check
            

Use for clarity; avoid chaining multiple comparisons—use oneOf instead.

oneOf(value, list)

ts
                html`${when(oneOf(mode, ['create', 'edit']), 'Writable', 'Read-only')}`
            

Use instead of long || chains. Avoid huge lists—precompute a Set if needed.

and(...conditions) / or(...conditions)

ts
                html`${when(and(isAuth, hasPlan), 'Welcome back')}`
            

Use to express intent; avoid very long chains—extract helper functions instead.

pick(obj, key, mapper?)

ts
                const [user] = state({ profile: { name: 'Ada' } })
html`<p>Name: ${pick(user, 'profile.name')}</p>`
html`<p>
    Upper: ${pick(user, 'profile.name', (v) => String(v).toUpperCase())}
</p>`
            

Use when passing nested data into templates without manual guarding. Avoid overusing for simple flat reads—direct getters are clearer.

element(tagFn, options)

ts
                const tag = () => (isBlock() ? 'div' : 'button')
const click = () => console.log('clicked')
html`${() =>
    element(tag(), {
        attributes: { class: 'box', onclick: click, 'data-kind': 'demo' },
        properties: { title: 'hello' },
        textContent: 'text',
        htmlContent: '<strong>Hi</strong>',
        children: [element('span', { textContent: ' more text' })],
    })}`
            

Use for dynamic tags or programmatic element creation. Avoid when a static tag suffices.

suspense(asyncAction, loadingTpl?, errorTpl?)

ts
                const loadTodos = () => fetch('/api/todos').then((r) => r.json())

html`
    ${suspense(
        async () => {
            const todos = await loadTodos()
            return html`<ul>
                ${repeat(todos, (t) => html`<li>${t.title}</li>`)}
            </ul>`
        },
        html`<p>Loading todos…</p>`,
        (err) => html`<p class="error">${err.message}</p>`
    )}
`
            

Use for async UI slots. Avoid calling the same slow action repeatedly; wrap it to cache results if needed.

val(anything)

ts
                const maybe = (x) => val(x) ?? 'N/A'
            

Use in utility code; avoid sprinkling in templates—other helpers already unwrap.

Patterns & Best Practices

More examples

ts
                const [gap] = state(12)
html`<div style="--gap:${() => `${gap()}px`}; margin: ${gap}px">Box</div>`
            
ts
                html`${when(oneOf(status, ['loading', 'creating']), 'Working…', 'Idle')}`
            
ts
                const groups = () => Object.groupBy(items(), (o) => o.group ?? 'Ungrouped')
html`${repeat(groups, ([g, opts]) => html`<section>${when(g, g)}</section>`)}`
            
ts
                const [todos, setTodos] = state([])
const [loading, setLoading] = state('idle' as 'idle' | 'loading' | 'error')

export const refresh = async () => {
    setLoading('loading')
    try {
        const list = await api.list()
        setTodos(list)
        setLoading('idle')
    } catch {
        setLoading('error')
    }
}
            
ts
                html`${count()}` // render once
html`${count}` // stays reactive to count changes
            

Resources

Happy building with Markup! Keep templates declarative, lean on the helpers, and let functions drive reactivity.

edit this doc