Oxpecker documentation

Oxpecker.ViewEngine

Oxpecker.ViewEngine is code-as-markup engine used to render your HTML views based on the F# feature called Computation Expressions.

Medium article: 7 reasons to try Oxpecker.ViewEngine

Nuget package dotnet add package Oxpecker.ViewEngine

Markup example:

open Oxpecker.ViewEngine

type Person = { Name: string }

let subView = p() { "Have a nice day" }

let mainView (model: Person) =
    html() {
        body(style="width: 800px; margin: 0 auto") {
            h1(style="text-align: center; color: red") {
                $"Hello, {model.Name}!"
            }
            subView
            ul() {
                for i in 1..10 do
                    br()
                    li().attr("onclick", $"alert('Test {i}')") {
                        span(id= $"span{i}", class'="test") { i }
                    }
            }
        }
    }

Documentation:

HtmlElement

HtmlElement is a main interface of Oxpecker.ViewEngine. It’s extended by two additional interfaces HtmlTag and HtmlContainer:

    type HtmlElement =
        abstract member Render: StringBuilder -> unit
    type HtmlTag =
        inherit HtmlElement
        abstract member AddAttribute: HtmlAttribute -> unit
    type HtmlContainer =
        inherit HtmlElement
        abstract member AddChild: HtmlElement -> unit
    ...

There are 7 types of HTML elements available: RegularNode, VoidNode (only attributes), FragmentNode (only children), RegularTextNode(escaped text), RawTextNode(unescaped text), IntNode(integer), PrerenderedNode(prerendered markup around children).

All HTML tags inherit from RegularNode or VoidNode and you can easily create your own tag:

type myTag() =
    inherit RegularNode("myTag") // will render <myTag></myTag>

HtmlElement holds two collections inside: attributes and children. More on them below.

Children

Regular nodes can have children that will be added to children collection as you write them between curly braces. Void nodes and Text nodes don’t have children. You can programmatically access Chidren property of any HtmlContainer.

let result = div() {
    br()
    span() { "Some text" }
}

let children = result.Children // br and span

Attributes

Regular and Void nodes can have attributes. Some general attributes are defined inside HtmlElement while each tag can have its specific attributes. This will prevent you from assigning attributes to the element that it doesn’t support. You can programmatically access Attributes property of any HtmlTag.

let result = div(class'="myClass") {
    br(id="1234") // href attribute won't work here
    a(href="/") { "Some link" }
}

let children = result.Attributes // class=myClass

You can also attach any custom attribute to the HtmlElement using .attr method:

div().attr("my-secret-key", "lk23j4oij234"){
    "Secret div"
}

For data-* attributes you can use dedicated method:

div().data("secret-key", "lk23j4oij234"){
    "Secret div"
} // renders <div data-secret-key="lk23j4oij234">Secret div</div>

Event handlers

Oxpecker.ViewEngine doesn’t provide attributes for javascript event handlers like onclick. This is done on purpose, since it would encourage people using them, which is rather an antipattern. However, if you really need it, you can always use .on method to achieve same goal.

ViewEngine will create html attribute with inline handler for you:

div().on("click", "alert('Hello')"){
    "Clickable div"
}
// <div onclick="alert('Hello')">Clickable div</div>

HTML escaping

Oxpecker.ViewEngine will escape text nodes and attribute values for you. However, sometimes it’s desired to render unescaped html string, in that case raw function is provided

div(){
    "<script></script>" // This will be escaped
    raw "<script></script>" // This will NOT be escaped
    123 // Numbers are NOT escaped
}

Prerendering

Views are object trees that are walked (and their text and attributes escaped) on every render. When a part of your view is static, prerender lets you pay that cost once: it renders an element together with all its children into a snapshot that is appended as a plain string on every subsequent render.

// rendered once, when the module is initialized
let pageHeader =
    prerender(
        header() {
            h1() { "My site" }
            nav() { a(href = "/") { "Home" } }
        }
    )

let page (model: Model) =
    html() {
        body() {
            pageHeader // appended as a plain string on every request
            main() { model.Content }
        }
    }

prerender returns a RawTextNode holding already-escaped HTML, so the snapshot is not escaped again when embedded.

Note that the snapshot is taken eagerly, at the moment of the call: children or attributes added to the original element afterwards won’t be reflected in the returned node.

When only a small part of the markup changes between renders, prerenderAround lets you prerender everything around it. It takes a function that places the provided hole inside your markup, renders the static part once, and gives you back a factory that is used like any other tag:

let layout =
    prerenderAround(fun content ->
        html() {
            body() {
                header() { h1() { "My site" } }
                main() { content }
                footer() { "(c) 2026" }
            }
        })

let page (model: Model) =
    layout() {
        h2() { model.Title }
        p() { model.Text }
    }

Everything outside the hole is rendered once, so every page call only appends two prerendered strings around its own children. The hole has to be used exactly once, otherwise prerenderAround raises an ArgumentException.

Rendering

There are several functions to render HtmlElement (after opening Oxpecker.ViewEngine namespace):

Aria

To enable ARIA attributes support you need to open Aria module:

open Oxpecker.ViewEngine.Aria

let x = span(
    role="checkbox",
    id="checkBoxInput",
    ariaChecked="false",
    tabindex=0,
    ariaLabelledBy="chk15-label"
)

Fragments

Sometimes you need to group several elements together without wrapping them in div or similar. You can use Fragment special tag for that:

let onlyChildren = Fragment() {
    span() { "one" }
    span() { "two" }
}

let parent = div() {
    onlyChildren
} // renders <div><span>one</span><span>two</span></div>