---
title: "Web Components for F#"
description: "Build standards-based web components in F# with Lit and Fable. Type-safe templates, Elmish, context and routing, compiled to lean JavaScript."
---

# Web Components for F#

Firelight is a set of F# bindings for [Lit](https://lit.dev/), compiled with [Fable](https://fable.io/). Write type-safe components with reactive properties, templates and the Elmish loop, and ship standard custom elements that work in any page.

- [Get started](https://roboz0r.github.io/Firelight/start/)
- [View on GitHub](https://github.com/roboz0r/Firelight)

## 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.

```fsharp
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"
```

```html
<my-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.

- **What you write is what runs.** There's no Firelight-specific compiler plugin or build step:
  `[<AttachMembers>]` tells Fable to put the members on the JavaScript class under their own
  names, where Lit looks for them.
- **Lit's features map directly.** Lifecycle callbacks, controllers and shadow DOM options are
  members you override or call on the class, so [Lit's documentation](https://lit.dev/docs/components/overview/)
  for them carries over.
- **The class is a thin shell.** Changing `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.

## 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.

```fsharp
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"
```

```html
<my-elmish-counter></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](https://roboz0r.github.io/Firelight/demos/todo/), Lit included, ships 24.2 kB of gzipped JavaScript.

[Firelight compared](https://roboz0r.github.io/Firelight/why/) 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.

| Package | What it does |
| --- | --- |
| [Firelight](https://roboz0r.github.io/Firelight/packages/firelight/) | Core bindings: `LitElement`, `html`/`css` templates, directives, reactive properties |
| [Firelight.Context](https://roboz0r.github.io/Firelight/packages/context/) | Share state across a component tree without passing it through every layer |
| [Firelight.Elmish](https://roboz0r.github.io/Firelight/packages/elmish/) | Run an Elmish (Model-View-Update) loop inside a component |
| [Firelight.Router](https://roboz0r.github.io/Firelight/packages/router/) | Client-side routing on the URL Pattern API, with routes as an F# union |
| [Firelight.Signals](https://roboz0r.github.io/Firelight/packages/signals/) | Shared reactive state that any component can read |
| [Firelight.Motion](https://roboz0r.github.io/Firelight/packages/motion/) | Animate elements as they move, appear and disappear |
| [Firelight.Observers](https://roboz0r.github.io/Firelight/packages/observers/) | Reactive controllers for resize, intersection, mutation and performance observers |
| [Firelight.Task](https://roboz0r.github.io/Firelight/packages/task/) | Async work with pending, complete and error states |
| [Firelight.Virtualizer](https://roboz0r.github.io/Firelight/packages/virtualizer/) | Long lists that only render the visible items |

## Demo apps

### [Todo](https://roboz0r.github.io/Firelight/demos/todo/)

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

### [Kanban](https://roboz0r.github.io/Firelight/demos/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](https://roboz0r.github.io/Firelight/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:

```sh
dotnet add package Firelight
npm install lit
```

New to Firelight? The [Get started](https://roboz0r.github.io/Firelight/start/) tutorial creates an app from a template,
then gives its component an attribute, an event, styles and a list.

<p class="notice">Firelight hasn't reached 1.0 yet, so its API may still change before then.</p>
