---
title: "Firelight.Observers"
description: "Reactive controllers for the browser's resize, intersection, mutation and performance observers. F# bindings for @lit-labs/observers."
---

# Firelight.Observers

Reactive controllers for the browser's resize, intersection, mutation and performance observers.

- [Lit Labs: @lit-labs/observers](https://github.com/lit/lit/tree/main/packages/labs/observers)
- [NuGet 0.3.0](https://www.nuget.org/packages/Firelight.Observers/0.3.0)
- [Source](https://github.com/roboz0r/Firelight/tree/main/src/Firelight.Observers)

## Why use it

The browser's observers report things a component often needs to know: its own size, whether it's
on screen, changes to its children. Using them directly means creating the observer when the
component connects, disconnecting it when it's removed, and asking Lit to render when something
changes.

Each controller here does that work. Give it a callback that turns the observer's entries into a
value, then read `.value` in `render`. CSS container queries can restyle a
component by its size; a `ResizeController` can also change what it renders.

## Example

The panel picks its layout from its own width, not the window's. Drag its bottom-right corner to resize it.

```fsharp
module Snippets.ResizePanel

open Fable.Core
open Firelight
open Firelight.Observers
open type Firelight.Lit

/// Lays itself out by its own width, not the window's. Drag the bottom-right corner to resize it.
[<AttachMembers>]
type ResizePanel() as this =
    inherit LitElement()

    let width =
        ResizeController<float>(
            this,
            ResizeControllerConfig(
                callback =
                    ResizeValueCallback(fun entries _ ->
                        if entries.Length = 0 then
                            0.0
                        else
                            entries.[0].contentRect.width
                    )
            )
        )

    static member styles =
        css
            $$"""
        :host {
            display: block; resize: horizontal; overflow: auto; box-sizing: border-box; width: 100%;
            min-width: 10rem; max-width: 100%; padding: 1rem;
            border: 1px dashed var(--border); border-radius: 0.5rem;
        }
        .boxes { display: grid; gap: 0.5rem; }
        .wide .boxes { grid-template-columns: repeat(3, 1fr); }
        .box { padding: 0.75rem; border-radius: 0.5rem; background: var(--accent); color: var(--accent-fg); text-align: center; }
        """

    override _.render() =
        let px = width.value |> Option.defaultValue 0.0
        let layout = if px >= 300.0 then "wide" else "narrow"

        html
            $"""
        <div class={layout}>
            <p>{int px}px wide, so the <strong>{layout}</strong> layout</p>
            <div class="boxes"><div class="box">One</div><div class="box">Two</div><div class="box">Three</div></div>
        </div>"""

defineElement<ResizePanel> "my-resize-panel"
```

```html
<my-resize-panel></my-resize-panel>
```

## Observe an element in the template

A controller observes its host unless told otherwise. To observe an element the component renders,
give the controller `target = null` and mark the element with a hole in its opening tag:
`<div {controller.target ()}>`. The element is observed while it's rendered. Scroll the box until
the marker shows:

```fsharp
module Snippets.ScrollMarker

open Fable.Core
open Firelight
open Firelight.Observers
open type Firelight.Lit

/// Says whether the marker inside its scrolling box is in view.
[<AttachMembers>]
type ScrollMarker() as this =
    inherit LitElement()

    // target = null: observe only the element that target () marks in the template, not the host.
    let marker =
        IntersectionController(
            this,
            IntersectionControllerConfig(
                target = null,
                callback = IntersectionValueCallback(fun entries _ -> entries |> Array.exists _.isIntersecting)
            )
        )

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.5rem; }
        p { margin: 0; }
        .box { height: 7rem; overflow-y: auto; border: 1px solid var(--border, #ccc); border-radius: 0.5rem; padding: 0 0.75rem; }
        .spacer { height: 14rem; padding-top: 0.5rem; }
        .marker { margin-bottom: 0.75rem; padding: 0.4rem 0.75rem; border-radius: 0.4rem; background: #1971c2; color: white; }
        """

    override _.render() =
        let inView = marker.value = Some true

        html
            $"""
        <p role="status">The marker is {if inView then "in view" else "out of view"}.</p>
        <div class="box" tabindex="0" aria-label="Scrolling box">
            <div class="spacer">Scroll down.</div>
            <div class="marker" {marker.target ()}>Marker</div>
        </div>"""

defineElement<ScrollMarker> "my-scroll-marker"
```

```html
<my-scroll-marker></my-scroll-marker>
```

`ResizeController.target` is Lit's own directive. `@lit-labs/observers` has none for
`IntersectionController`, so Firelight's is Lit's `ref` with one callback per controller: it
observes the element a render puts there, and stops observing one a render removes. That makes it
one element per controller. Put `target ()` on two, and Lit moves the ref between them on every
render, so only the last is observed, and the component renders again and again. For several
elements, give each its own controller.

## Install

```sh
dotnet add package Firelight.Observers
npm install @lit-labs/observers
```
