Web Components for F#

Firelight is a set of F# bindings for Lit, compiled with Fable. Write type-safe components with reactive properties, templates and the Elmish loop, and ship standard custom elements that work in any page.

Get startedView on GitHub

A component in a few lines

The code below is the source file for the live component beside it. This site is built with Firelight.

module Snippets.Counter

open Fable.Core
open Firelight
open type Firelight.Lit

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

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

    static member styles =
        css
            $$"""
        button { font: inherit; padding: 0.5rem 1rem; cursor: pointer; }
        """

    member val count = 0 with get, set

    override this.render() =
        html
            $"""
        <button @click={fun _ -> this.count <- this.count + 1}>
            Clicked {this.count} times
        </button>"""

defineElement<Counter> "my-counter"

Why a class?

You write a component as an F# class with mutable properties, which can look out of place in F#. But a custom element is a class: the browser creates it, calls its lifecycle methods and sets its properties, and Lit's components extend LitElement. Firelight keeps that mapping direct: a component is an ordinary F# class.

The Elmish loop, inside a component

ElmishController runs Model-View-Update inside a component. The update function stays pure and every new model triggers a render.

module Snippets.ElmishCounter

open Fable.Core
open Firelight
open Firelight.Elmish
open type Firelight.Lit

type Model = { Count: int }

type Msg =
    | Increment
    | Decrement

let init () = { Count = 0 }

let update msg model =
    match msg with
    | Increment -> { model with Count = model.Count + 1 }
    | Decrement -> { model with Count = model.Count - 1 }

[<AttachMembers>]
type ElmishCounter() as this =
    inherit LitElement()

    let elmish = ElmishController.simple this init update

    static member styles =
        css
            $$"""
        :host { display: inline-flex; align-items: center; gap: 0.75rem; }
        button { font: inherit; width: 2.5rem; height: 2.5rem; cursor: pointer; }
        """

    override _.render() =
        html
            $"""
        <button @click={fun _ -> elmish.dispatch Decrement}>−</button>
        <output>{elmish.model.Count}</output>
        <button @click={fun _ -> elmish.dispatch Increment}>+</button>"""

defineElement<ElmishCounter> "my-elmish-counter"

Why Firelight

Built on web standards

Components are native custom elements with Shadow DOM. Nothing takes over the page, so you can use them in plain HTML or alongside any other framework.

Idiomatic F#

Records, discriminated unions and pattern matching drive the UI. Immutable models mean every change is seen by Lit's change detection.

Works with other web components

Libraries like Web Awesome, Fluent UI and Carbon are also web components, so you use their tags directly in your templates without wrappers.

No runtime of its own

The core bindings compile away: your components import Lit directly. The Todo demo, Lit included, ships 24.2 kB of gzipped JavaScript.

Firelight compared sets it beside Fable.Lit, Feliz, Sutil and Lit in TypeScript.

Packages

Start with Firelight and add the others as you need them. Each page explains when you'd want the package and has a live example.

PackageWhat it does
FirelightCore bindings: LitElement, html/css templates, directives, reactive properties
Firelight.ContextShare state across a component tree without passing it through every layer
Firelight.ElmishRun an Elmish (Model-View-Update) loop inside a component
Firelight.RouterClient-side routing on the URL Pattern API, with routes as an F# union
Firelight.SignalsShared reactive state that any component can read
Firelight.MotionAnimate elements as they move, appear and disappear
Firelight.ObserversReactive controllers for resize, intersection, mutation and performance observers
Firelight.TaskAsync work with pending, complete and error states
Firelight.VirtualizerLong lists that only render the visible items

Demo apps

Todo

An Elmish loop in the root component, with state and dispatch shared through context. 24.2 kB of gzipped JavaScript.

Kanban

A board with drag and drop between columns, an edit dialog and Tailwind styles, saved to local storage as you go. 34.1 kB of gzipped JavaScript.

Client-side routing

URL patterns matched to an F# route type with Firelight.Router, and why the rest of this site doesn't use it.

Get started

Add the package to a Fable project, and install Lit from npm:

dotnet add package Firelight
npm install lit

New to Firelight? The Get started tutorial creates an app from a template, then gives its component an attribute, an event, styles and a list.

Firelight hasn't reached 1.0 yet, so its API may still change before then.