A strongly‑typed HTML DSL for Fable.Lit, offering a clean F# syntax for Lit components and a foundation for building custom DSL libraries.
$ dotnet add package Fable.Lit.DslA collection of expressive, type-safe DSLs for building UI with Fable.Lit. This repo currently includes:
More DSLs may be added over time.
Add the packages to your project:
dotnet add package Fable.Lit.Dsl
dotnet add package Fable.Lit.Dsl.Shoelace
If using Shoelace, register its assets at app startup:
open Fable.Core.JsInterop
open Fable.Lit.Dsl.Shoelace
// Set the base path for Shoelace assets (icons, etc.)
Shoelace.setBasePath()
// Import the components you need
Shoelace.startImports [|
importDynamic Shoelace.Asset.Button
importDynamic Shoelace.Asset.Input
importDynamic Shoelace.Asset.Dialog
// ... add more as needed
|]
The HTML DSL provides a natural, structured way to write Lit templates in F# without stringly-typed markup.
open Fable.Lit.Dsl
view {
h1 { "Hello, world!" }
p { "This is the Fable.Lit HTML DSL." }
button {
disabled false
onClick (fun _ -> dispatch Increment)
"Click me"
}
}
view { } - Use at the top level of components. Returns a TemplateResult for rendering.template { } - Use for nested fragments inside elements. Returns a Node.el "tag-name" { } - Create custom elements with any tag name.elUse el for one-off custom elements or web components:
view {
el "my-custom-element" {
attr "theme" "dark"
prop "config" {| rows = 10; cols = 5 |}
boolAttr "enabled" true
on "custom-event" (fun e -> dispatch (CustomEvent e))
"Child content"
}
}
Available attribute helpers:
attr "name" value - String attributeboolAttr "name" true - Boolean attribute (present when true, absent when false)prop "name" value - JavaScript property (for complex values or web component properties)on "event-name" handler - Event handlertemplateUse template when you need to return multiple sibling elements without a wrapper element. This is similar to React fragments (<>...</>).
/// Returns multiple elements without a wrapper div
let userInfo (user: User) =
template {
dt { "Name" }
dd { user.Name }
dt { "Email" }
dd { user.Email }
}
let mainView model =
view {
h1 { "User Details" }
dl {
// Inserts dt/dd pairs directly into the dl, no wrapper element
userInfo model.User
}
}In most cases, wrapping content in a div is fine. Use template only when an extra wrapper element would break your HTML structure or CSS styling (like inside <dl>, <table>, <ul>, or flex/grid containers where extra elements affect layout).
Typed, ergonomic bindings for the Shoelace Web Component library.
open Fable.Lit.Dsl
open Fable.Lit.Dsl.Shoelace
view {
slButton {
variantPrimary
onClick (fun _ -> dispatch Save)
"Save"
}
slInput {
label' "Email"
placeholder' "you@example.com"
onSlInput (fun e -> dispatch (EmailChanged e))
}
slDialog {
label' "Confirm"
open' model.ShowDialog
onSlRequestClose (fun _ -> dispatch CloseDialog)
p { "Are you sure?" }
}
}slButton, slInput, slDialog, slDrawer, etc.)variant, size, disabled', open', label', etc.)onSlChange, onSlInput, onSlShow, onSlHide, etc.)// Variants
variantPrimary // or: variant "primary"
variantSuccess
variantDanger
variantWarning
variantNeutral
// Sizes
sizeSmall // or: size "small"
sizeMedium
sizeLarge
// States
disabled' true
loading true
open' true
checked' true
clearable true
// Values
value' "text"
label' "Label"
placeholder' "Placeholder"
helpText "Help text"onSlChange handler // Value changed (after interaction)
onSlInput handler // Real-time input
onSlShow handler // Element starting to show
onSlAfterShow handler // Element shown, animations complete
onSlHide handler // Element starting to hide
onSlAfterHide handler // Element hidden, animations complete
onSlRequestClose handler // Close requested (dialogs/drawers)
onSlSelect handler // Menu item selectedThe DSL system is intentionally modular. You can create your own DSL for any Web Component library.
module MyComponents
open Fable.Lit.Dsl
// Define elements for your web components
let fancyCard = ElementBuilder("fancy-card")
let fancyButton = ElementBuilder("fancy-button")
// Define typed properties
let cardTitle (text: string) = prop "cardTitle" text
let elevation (level: int) = prop "elevation" level
// Define typed events
let onFancyClick (handler: obj -> unit) : Attr = Event("fancy-click", handler)Usage:
open MyComponents
view {
fancyCard {
cardTitle "Welcome"
elevation 2
fancyButton {
onFancyClick (fun _ -> dispatch Click)
"Click me"
}
}
}You can extend:
ElementBuilder)attr)prop)Event)boolAttr)This repo models the pattern used by the HTML and Shoelace DSLs.
view {
h2 { "Dashboard" }
if model.IsLoading then
slSpinner { }
else
div {
p { $"Welcome, {model.Username}!" }
}
}view {
ul {
for item in model.Items do
li { item.Name }
}
}view {
header {
class' "app-header"
h1 { "My App" }
}
main {
slCard {
div {
slot' "header"
h3 { "Stats" }
}
p { $"Count: {model.Count}" }
div {
slot' "footer"
slButton {
variantPrimary
onClick (fun _ -> dispatch Increment)
"Increment"
}
}
}
}
}let inputRef = ref Unchecked.defaultof<Browser.Types.HTMLInputElement>
view {
slInput {
bindRef (fun el -> inputRef.Value <- el :?> _)
label' "Focus me"
}
slButton {
onClick (fun _ -> inputRef.Value.focus())
"Focus Input"
}
}open Lit
view {
h1 { "Mixed Content" }
// Embed an existing Lit template
lit (html $"<p>Raw Lit template</p>")
// Or raw HTML (use sparingly)
rawHtml "<p>Raw HTML string</p>"
}The DSL system is intentionally modular, and this repo may grow over time as more UI libraries adopt Web Components.