---
title: "Templates"
description: "Write templates as plain HTML in F# interpolated strings: bindings, conditionals, lists, directives and static values, and the F# mistakes to avoid."
---

# Templates

Firelight templates are plain HTML written inside F# interpolated strings. Passing them to Lit's `html` tag function enables efficient rendering and surgical DOM updates. The bindings are Lit's too, so what you know about HTML and Lit carries over. This guide covers the parts that are F#.

- [Lit docs: Templates](https://lit.dev/docs/templates/overview/)
- [Lit docs: Expressions](https://lit.dev/docs/templates/expressions/)
- [Lit docs: Built-in directives](https://lit.dev/docs/templates/directives/)

A template is the value `html` returns: a description of some DOM that Lit renders, then updates
when the values in it change. A component returns one from `render`, and so can any function. Most
of a user interface can be ordinary F# functions that take data and return templates. Only the
parts that need their own state or lifecycle have to be components.

## Templates are interpolated strings

```fsharp
open Firelight
open type Firelight.Lit

let greeting (name: string) =
    html $"""<p class="greeting">Hello, {name}.</p>"""
```

`open type Firelight.Lit` brings `html`, `css`, `nothing` and the directives into scope.

Lit's `html` is a JavaScript tagged template, which keeps a template's fixed text apart from the
values in its holes, such as `{name}` above. An F# interpolated string passed to `html` is a `FormattableString`, which makes
the same split, and [Fable](https://fable.io/) compiles it to a tagged template. Lit sees exactly what it would see from
JavaScript: it parses each template once, and on later renders it updates only the values that
changed.

The split also makes templates safe by default. Values are never pasted into the HTML. A string in
a hole becomes text or an attribute value, so a `<script>` tag in someone's name is shown, not run.
For the same reason you can't build a template with `+` or `sprintf`: `html` only accepts an
interpolated string.

Lit doesn't parse the markup itself. The first time a template renders, Lit joins its fixed text
with a marker in place of each hole and hands the result to the browser's own HTML parser, through
a `<template>` element. Lit clones the parsed DOM wherever the template renders, and the markers
tell it which nodes and attributes each hole updates. So a hole only works where the parser leaves
a marker Lit can find. A hole works:

- between tags, as child content;
- in an attribute value, whole or in part;
- in an element's opening tag, as an element directive;
- inside `<title>`, `<style>` or `<script>` (for a component's styles, prefer `css`).

A hole doesn't work:

- in a tag name or an attribute name (see [Static values](https://roboz0r.github.io/Firelight/guides/templates/#static-values));
- inside an HTML comment, where it isn't updated;
- inside a `<template>` element's content, where Lit throws in development builds;
- inside a `<textarea>` or a `contenteditable` element, where typing breaks Lit's markers. Bind
  `.value` or `.innerText` instead.

Where a hole goes also decides what it sets, which the next section covers.

A hole takes any F# expression: a property, a function call, an `if`, another template. Triple
quotes let the HTML use `"` around attribute values.

In `$"""`, a brace opens a hole, and a literal brace has to be doubled. CSS is full of braces, so
styles use `$$"""` instead. With two dollar signs, single braces are plain text and a hole takes
two: `{{gap}}`.

```fsharp
open Firelight
open type Firelight.Lit

let gap = css $"0.75rem"

let styles =
    css
        $$"""
    :host { display: flex; gap: {{gap}}; }
    button { font: inherit; }
    """
```

`css` only accepts other `css` values and numbers in its holes. `svg $"""..."""` works like `html`,
for fragments that go inside an `<svg>` element.

## Binding values

Where a hole sits decides what it sets. These are Lit's rules; F# only changes the delimiters,
`{value}` where JavaScript has `${value}`.

| Binding | Sets | Example |
|---|---|---|
| `{value}` between tags | Text, a template, a list of them, or nothing | `<p>Hello, {name}</p>` |
| `attr={value}` | An attribute, as a string | `<meter value={used}>` |
| `.prop={value}` | A DOM property, of any type | `<input .value={text}>` |
| `?attr={flag}` | A boolean attribute: present when `true`, removed when `false` | `<button ?disabled={busy}>` |
| `@event={handler}` | An event listener | `<button @click={fun _ -> save ()}>` |
| `<tag {directive}>` | An element directive, such as `ref` | `<input {ref field}>` |

Quotes around an attribute's value are optional. Use them to put fixed text next to a hole:
`class="bar {extra}"`.

This example uses each kind of binding except the last:

```fsharp
module Snippets.TemplateBindings

open Fable.Core
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type NameField() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "name", PropertyDeclaration<string>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.75rem; justify-items: start; }
        input, button { font: inherit; padding: 0.3rem 0.6rem; }
        p { margin: 0; }
        """

    member val name = "" with get, set

    override this.render() =
        html
            $"""
        <label>
            Name
            <input maxlength="20" .value={this.name}
                @input={Ev.value (fun name -> this.name <- name)}>
        </label>
        <meter max="20" value={this.name.Length} aria-label="Characters used"></meter>
        <button ?disabled={this.name = ""} @click={fun _ -> this.name <- ""}>Clear</button>
        <p>Hello, {if this.name = "" then "whoever you are" else this.name}.</p>"""

defineElement<NameField> "my-name-field"
```

```html
<my-name-field></my-name-field>
```

The text box binds `.value`, the property, rather than the `value` attribute. The attribute is
only the box's starting value: once you've typed in it, changing the attribute doesn't change what
it shows, so Clear would empty `name` and leave your text where it was. Bind attributes for what
you would write in HTML, and properties for live state and for values that aren't strings, such as
a list passed to a child component.

A lambda in a hole is a new function on every render. That's fine: Lit keeps one listener on the
element and calls the newest function, so a handler always sees the current values. For listener
options such as `passive` or `once`, bind a `LitEventListener(handler, passive = true)` instead of
a function.

### Typed event handlers

A hole's type is `obj`, so F# can't tell what `e` is in `@keydown={fun e -> ...}`, and you would
annotate it: `fun (e: KeyboardEvent) -> ...`. The `Ev` module gives the type instead.
`Ev.keyboard (fun e -> ...)` makes `e` a `KeyboardEvent`, with completion for `e.key`. It returns
the handler it's given, so the JavaScript is just the lambda.

```fsharp
open Firelight
open type Firelight.Lit

let search (query: string) (setQuery: string -> unit) =
    html
        $"""
    <input type="search" .value={query}
        @input={Ev.value setQuery}
        @keydown={Ev.keyboard (fun e -> if e.key = "Escape" then setQuery "")}>"""
```

`Ev.value` hands your function the field's new `value`, and `Ev.checked'` a checkbox's `checked`
(with a quote, as `checked` is reserved in F#). Both read the element the binding is on, so they
replace the `(e.target :?> HTMLInputElement).value` downcast for an `<input>`, `<select>` or
`<textarea>`. There are also `Ev.mouse`, `Ev.pointer`, `Ev.focus`, `Ev.input`, `Ev.wheel`,
`Ev.drag`, `Ev.touch`, `Ev.submit`, and `Ev.event` for any other event.

`Ev.custom<'T>` types a `CustomEvent` whose `detail` is a `'T`, such as one a component raises with
`Event.customEvent`. Its `detail` is a `'T option`. Here a text box raises `tag-added` when you
press Enter, and the list adds the tag:

```fsharp
module Snippets.TemplateEvents

open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

/// A text box that raises `tag-added`, with the tag as its detail, when Enter is pressed.
[<AttachMembers>]
type TagInput() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "text", PropertyDeclaration<string>(state = true) ]

    static member styles = css $$"""input { font: inherit; padding: 0.3rem 0.6rem; }"""

    member val text = "" with get, set

    member this.Add() =
        let tag = this.text.Trim()

        if tag <> "" then
            this.dispatch (Event.customEvent ("tag-added", tag))
            this.text <- ""

    override this.render() =
        // Ev.keyboard makes `e` a KeyboardEvent, so `e.key` needs no annotation.
        let addOnEnter =
            Ev.keyboard (fun e ->
                if e.key = "Enter" then
                    this.Add()
            )

        html
            $"""
        <input aria-label="New tag" placeholder="Add a tag" .value={this.text}
            @input={Ev.value (fun text -> this.text <- text)} @keydown={addOnEnter}>"""

[<AttachMembers>]
type TagList() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "tags", PropertyDeclaration<string list>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.75rem; justify-items: start; }
        ul { margin: 0; padding-left: 1.25rem; }
        """

    member val tags = [ "fsharp"; "lit" ] with get, set

    override this.render() =
        // The tag-added event's detail is the tag: a string option, as an event may have no detail.
        let addTag =
            Ev.custom<string>(fun e -> e.detail |> Option.iter (fun tag -> this.tags <- this.tags @ [ tag ]))

        html
            $"""
        <my-tag-input @tag-added={addTag}></my-tag-input>
        <ul>{this.tags |> List.map (fun tag -> html $"<li>{tag}</li>")}</ul>"""

defineElement<TagInput> "my-tag-input"
defineElement<TagList> "my-tag-list"
```

```html
<my-tag-list></my-tag-list>
```

Nothing checks that the function matches the event: `@click={Ev.keyboard (fun e -> ...)}`
compiles, because the event's name is fixed text that only Lit reads. Nor does anything check that a
custom event's `detail` is the type you name.

## Conditionals and lists

Templates have no syntax of their own for conditions or loops. `if`, `match` and the list
functions already do the job, and a template is a value like any other.

```fsharp
module Snippets.TemplateLists

open Fable.Core
open Firelight
open type Firelight.Lit

// A template is a function: it takes what it shows and returns html.
let private itemView (remove: int -> unit) (index: int) (fruit: string) =
    html $"""<li>{fruit} <button @click={fun _ -> remove index}>Remove</button></li>"""

[<AttachMembers>]
type Basket() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "fruits", PropertyDeclaration<string list>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.75rem; justify-items: start; }
        button { font: inherit; padding: 0.2rem 0.6rem; }
        ul { margin: 0; padding-left: 1.25rem; }
        p { margin: 0; }
        """

    member val fruits = [ "Apple"; "Pear" ] with get, set

    member this.Remove(index: int) =
        this.fruits <- List.removeAt index this.fruits

    member this.Add(fruit: string) = this.fruits <- this.fruits @ [ fruit ]

    override this.render() =
        let contents =
            match this.fruits with
            | [] -> html $"<p>The basket is empty.</p>"
            | fruits -> html $"<ul>{fruits |> List.mapi (itemView this.Remove)}</ul>"

        let emptyButton =
            if not this.fruits.IsEmpty then
                html $"""<button @click={fun _ -> this.fruits <- []}>Empty the basket</button>"""
            else
                nothing

        html
            $"""
        <div>
            <button @click={fun _ -> this.Add "Apple"}>Add an apple</button>
            <button @click={fun _ -> this.Add "Pear"}>Add a pear</button>
        </div>
        {contents}
        {emptyButton}"""

defineElement<Basket> "my-basket"
```

```html
<my-basket></my-basket>
```

Each branch of the `match` returns a template. `nothing` is Lit's marker for "render nothing
here", and it fits wherever a template does, so `emptyButton` needs no type annotation.

A hole renders any F# list, array or `seq`, so `List.map` is all a list needs. A list expression
such as `[ for fruit in fruits -> html $"<li>{fruit}</li>" ]` works too.

### Keyed lists with repeat

Lit updates a list by position. The first item's template updates the first element, and so on;
only the end of the list grows or shrinks. That's fast, and right whenever an element shows nothing
but your data. It goes wrong when an element holds state of its own, such as text typed into an
unbound box, focus or a running animation. Remove the first item, and each element is reused for
the item that came after it, while its state stays where it was.

`repeat` gives each item a key and moves its element with it. Type a note next to Bob in both
lists, then remove the first person:

```fsharp
module Snippets.TemplateRepeat

open Fable.Core
open Firelight
open type Firelight.Lit

let private everyone = [ "Ann"; "Bob"; "Cara" ]

// The note box isn't bound to anything: what you type lives only in the DOM.
let private row (name: string) =
    html $"""<li><label>{name} <input size="8"></label></li>"""

[<AttachMembers>]
type RepeatComparison() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [ "people", PropertyDeclaration<string list>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 1rem; }
        .lists { display: flex; flex-wrap: wrap; gap: 2rem; }
        p { margin: 0 0 0.4rem; font-weight: 600; }
        ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.4rem; }
        button, input { font: inherit; }
        """

    member val people = everyone with get, set

    override this.render() =
        html
            $"""
        <div>
            <button ?disabled={this.people.IsEmpty} @click={fun _ -> this.people <- List.tail this.people}>
                Remove the first person
            </button>
            <button @click={fun _ -> this.people <- everyone}>Reset</button>
        </div>
        <div class="lists">
            <div>
                <p>List.map</p>
                <ul>{this.people |> List.map row}</ul>
            </div>
            <div>
                <p>repeat</p>
                <ul>{repeat (this.people, (fun name _ -> name), (fun name _ -> row name))}</ul>
            </div>
        </div>"""

defineElement<RepeatComparison> "my-repeat-comparison"
```

```html
<my-repeat-comparison></my-repeat-comparison>
```

Use `repeat` when items are inserted, removed or reordered and their elements hold state, or are
components. Otherwise `List.map` is simpler and does less work. The key must be unique and stay
with the item, like an id. An item's position isn't a key.

`repeat` takes its key and template functions in two forms: of the item and its index,
`fun item index -> ...`, as in Lit, or of the item alone. A method fits either form if its
arguments match: `repeat (people, (fun p -> p.Id), this.PersonView)` for
`member this.PersonView(p: Person)`. A method that takes the item and the index as a tuple,
`member this.Row(p, i)`, doesn't convert to either; wrap it: `fun p i -> this.Row(p, i)`.

## Directives

Directives are functions that take over how a binding is applied. They are static members of
`Lit`, so `open type Firelight.Lit` brings them into scope, and Fable imports only the ones you
use. These are the ones you'll reach for most:

| Directive | Use it to | Example |
|---|---|---|
| `classMap` | Switch classes on and off | `class={classMap (ClassInfo.create [ "done", isDone ])}` |
| `styleMap` | Set inline styles; `None` removes one | `style={styleMap (StyleInfo.create [ "width", Some "40%" ])}` |
| `ref` | Reach the rendered element | `<input {ref field}>` with `let field = createRef<HTMLInputElement> ()` |
| `ifDefined` | Leave out an attribute when its value is `None` | `href={ifDefined link}` |
| `live` | Compare with the element's current value, not the last one rendered | `.value={live text}` |
| `repeat` | Key a list, so elements move with their items | [Keyed lists](https://roboz0r.github.io/Firelight/guides/templates/#keyed-lists-with-repeat) |
| `keyed` | Replace an element, rather than update it, when a key changes | `keyed (user.Id, profile user)` |
| `guard` | Skip re-rendering until a dependency changes | `guard ([\| rows; sortColumn \|], fun () -> table rows sortColumn)` |
| `cache` | Keep the DOM of templates you switch between, such as tabs | `cache (tabView tab)` |
| `unsafeHTML` | Render a trusted string as HTML | `unsafeHTML trustedHtml` |

Firelight also binds `join`, `until`, `asyncAppend`, `asyncReplace`, `templateContent` and
`unsafeSVG`, and Lit's `when`, `choose`, `map` and `range`, with `when` as `when'` because `when`
is an F# keyword. In F#, `if`, `match`, `List.map` and `[ 0 .. n - 1 ]` do the same jobs as those
four and read better. For data that loads asynchronously, [Firelight.Task](https://roboz0r.github.io/Firelight/packages/task/) is
usually a better fit than `until`.

This volume control uses the three most common: `classMap` stripes the bar when it's loud,
`styleMap` sets its width, and `ref` lets Reset put the focus back on the slider.

```fsharp
module Snippets.TemplateDirectives

open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type VolumeControl() =
    inherit LitElement()

    let slider = createRef<HTMLInputElement> ()

    static member properties =
        PropertyDeclarations.create [ "level", PropertyDeclaration<int>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.75rem; justify-items: start; }
        .track { width: 14rem; height: 0.75rem; border: 1px solid var(--border); border-radius: 1rem; overflow: hidden; }
        .bar { height: 100%; background: var(--accent); }
        .bar.loud { background: repeating-linear-gradient(45deg, var(--accent) 0 6px, var(--fg) 6px 12px); }
        button { font: inherit; padding: 0.2rem 0.6rem; }
        """

    member val level = 50 with get, set

    member this.Reset() =
        this.level <- 50
        // The ref's value is the rendered <input>, once there is one.
        slider.value |> Option.iter (fun input -> input.focus ())

    override this.render() =
        let bar = ClassInfo.create [ "loud", this.level > 80 ]
        let width = StyleInfo.create [ "width", Some(string this.level + "%") ]

        html
            $"""
        <label>
            Volume
            <input type="range" min="0" max="100" {ref slider} .value={string this.level}
                @input={Ev.value (fun level -> this.level <- int level)}>
        </label>
        <div class="track"><div class="bar {classMap bar}" style={styleMap width}></div></div>
        <button @click={fun _ -> this.Reset()}>Reset</button>"""

defineElement<VolumeControl> "my-volume-control"
```

```html
<my-volume-control></my-volume-control>
```

`classMap` and `styleMap` must be the only hole in their attribute, though fixed text can sit
beside them, as `bar` does. Each `classMap` key is a single class name. A key with a space in it,
such as `"bg-red text-white"`, renders the first time, then throws once its value changes.

A ref's `value` is `None` until the element has rendered, so read it in event handlers or after an
update, not in `render`. Use it for what a template can't say: focus, measuring, or calling a method
such as a dialog's `showModal`.

An F# option in an attribute hole doesn't remove the attribute. `None` becomes JavaScript
`undefined`, which Lit writes as an empty attribute. `ifDefined` removes it instead:

```fsharp
open Firelight
open type Firelight.Lit

let link (label: string) (href: string option) =
    html $"""<a href={ifDefined href}>{label}</a>"""
```

`live` is for a property the user can change. Lit only sets a binding when its value differs from
the last render. If the user has typed into a box but your state still holds the value you last
rendered, rendering it again sets nothing, and the typing stays. `live` compares with what the box
holds now.

A directive that fills an element's content, such as `keyed`, `cache`, `repeat`, `guard` or
`until`, returns a `ChildDirectiveResult`, a kind of `ChildRenderable`. So `render`, or a template
function, can return one without a template around it:

```fsharp
open Fable.Core
open Firelight
open type Firelight.Lit

type Profile = { Id: int; Name: string }

[<AttachMembers>]
type ProfileEditor() =
    inherit LitElement()

    member val profile: Profile option = None with get, set

    override this.render() =
        match this.profile with
        | None -> nothing
        // A new id gets a new input, so nothing typed for the last profile carries over.
        | Some p -> keyed (p.Id, html $"""<input aria-label="Name" value={p.Name}>""")
```

The directives that only work inside a tag, `ref`, `classMap`, `styleMap` and `live`, return a
plain `DirectiveResult`, which `render` doesn't accept.

### unsafeHTML

Some HTML arrives as a string, such as Markdown converted to HTML at build time. `unsafeHTML`
renders it as HTML rather than as text.

```fsharp
open Firelight
open type Firelight.Lit

/// `articleHtml` comes from our own Markdown build, never from users.
let article (articleHtml: string) =
    html $"""<article>{unsafeHTML articleHtml}</article>"""
```

Never pass it text that someone else controls. The string becomes part of your page, so markup
such as `<img src=x onerror="...">` runs that person's script with your page's permissions. If the
HTML comes from users, clean it with a sanitizer built for the job, such as DOMPurify, or show it
as text in an ordinary hole.

## Static values

Because the browser parses a template's fixed text, a tag name or an attribute name has to be part
of that text, not a hole. When one really must vary, such as the heading level
of a card that appears at different depths, use `StaticHTML`:

```fsharp
open Firelight
open type Firelight.Lit

type Level =
    | H2
    | H3

let card (level: Level) (title: string) (body: string) =
    let heading =
        match level with
        | H2 -> StaticHTML.literal $"h2"
        | H3 -> StaticHTML.literal $"h3"

    StaticHTML.html $"""<article><{heading}>{title}</{heading}><p>{body}</p></article>"""
```

`StaticHTML.html` is the `html` from `lit/static-html.js`; the plain `html` doesn't accept static
values. It returns an `HTMLTemplateResult`, as `html` does, so the two mix freely. Call it by its
full name, since `open type StaticHTML` would hide Lit's `html`.

Static values are part of the template, so each different value makes a separate template. When
the value changes, Lit throws away the old DOM and builds new DOM, rather than updating it. With
only a few cases, a `match` that returns a whole template for each does the same job without
static values.

Choose `StaticHTML.literal` values from a fixed set, as `card` does. `StaticHTML.unsafeStatic`
turns any string into template text, so, like `unsafeHTML`, it must never see untrusted input.

## Common mistakes

The compiler catches some template mistakes, though its message doesn't always point at the
cause:

| You wrote | The compiler says | Write instead |
|---|---|---|
| `html """<p>Hi</p>"""` | The type 'string' is not compatible with the type 'System.FormattableString' | `html $"""<p>Hi</p>"""` |
| `css $"""p { margin: 0; }"""` | Unexpected symbol ';' in expression, or another parse error | `css $$"""p { margin: 0; }"""` |
| `html $"""<p style="width: 50%">"""` | Invalid interpolated string. Bad format specifier | `50%%` in `$"""`. In `$$"""`, `%` is plain text |
| `if empty then nothing else html $"..."` | All branches of an 'if' expression must return values implicitly convertible to the type of the first branch, which here is 'nothing' | The template first: `if not empty then html $"..." else nothing` |
| `ref 0` | No overloads match for method 'ref' | `Operators.ref 0`: Lit's `ref` hides F#'s |

All branches of an `if` or `match` must fit one type, which F# takes from the first branch.
`nothing` fits where a template is expected, so a template first needs nothing extra. When
`nothing` comes first, annotate the expression as `ChildRenderable` (the interface for anything Lit
can render):

```fsharp fragment
let button: ChildRenderable =
    if isEmpty then nothing else html $"<button>...</button>"
```

Others compile, because a hole's type is `obj` and it accepts any value:

| You wrote | What happens | Write instead |
|---|---|---|
| `@click={this.Save()}` | `Save` runs on every render, and clicking does nothing | `@click={fun _ -> this.Save()}` |
| `@click={fun (e: KeyboardEvent) -> ...}` | The handler gets a click event, and `e.key` is `undefined` | The event's real type |
| `{price:N2}` | Debug builds, such as `npm run dev`, throw. Release builds drop the format | `{price.ToString "N2"}` |
| `{name}` in `$$"""` | Shows the text `{name}` | `{{name}}` |
| `title={maybeTitle}` with an option | `None` leaves an empty `title` attribute | `title={ifDefined maybeTitle}` |
| `this.items.Add item`, on a `ResizeArray` | No re-render | An F# list and `this.items <- this.items @ [ item ]` |

The last row needs a word. Lit re-renders when a reactive property gets a new value, and it decides
by comparing the old and new values by identity. F# lists, records and maps are immutable, so a
change always produces a new value and Lit always notices. A `ResizeArray`, an array or a mutable
field changed in place is still the same object, so nothing happens. Assign a new value, or call
`this.requestUpdate()` after the change.

The same goes for a `mutable` local in a template function. The function has already returned its
template, and changing the variable later doesn't call it again. State that affects what's shown
belongs in a reactive property or an Elmish model.
