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.
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.
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"
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.
[<AttachMembers>] tells Fable to put the members on the JavaScript class under their own
names, where Lit looks for them.this.count, a declared reactive property, schedules a
render. Keep domain logic in functions and state in immutable values; the next example moves the
state into an Elmish loop.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"
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.
Records, discriminated unions and pattern matching drive the UI. Immutable models mean every change is seen by Lit's change detection.
Libraries like Web Awesome, Fluent UI and Carbon are also web components, so you use their tags directly in your templates without wrappers.
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.
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.
| Package | What it does |
|---|---|
Firelight | Core bindings: LitElement, html/css templates, directives, reactive properties |
Firelight.Context | Share state across a component tree without passing it through every layer |
Firelight.Elmish | Run an Elmish (Model-View-Update) loop inside a component |
Firelight.Router | Client-side routing on the URL Pattern API, with routes as an F# union |
Firelight.Signals | Shared reactive state that any component can read |
Firelight.Motion | Animate elements as they move, appear and disappear |
Firelight.Observers | Reactive controllers for resize, intersection, mutation and performance observers |
Firelight.Task | Async work with pending, complete and error states |
Firelight.Virtualizer | Long lists that only render the visible items |
An Elmish loop in the root component, with state and dispatch shared through context. 24.2 kB of gzipped JavaScript.
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.
URL patterns matched to an F# route type with Firelight.Router, and why the rest of this site doesn't use it.
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.