- F# 99.1%
- Shell 0.9%
|
|
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| .github/workflows | ||
| Benchmark | ||
| data/css-spec | ||
| Fun.Css | ||
| Fun.Css.Tests | ||
| LOGOS | ||
| scripts | ||
| .build-image | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| build | ||
| build.fsx | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Directory.Build.props | ||
| Fun.Css.sln | ||
| global.json | ||
| LICENSE | ||
| README.md | ||
Fun.Css 
Patched build of slaveOftime/Fun.Css distributed via Forgejo. Tracks upstream master with PR-equivalent fixes pending merge.
Upstream sources (click through to view on GitHub):
- github.com/slaveOftime/Fun.Css — the original library this fork tracks
- github.com/slaveOftime/Fun.Blazor — the F# Blazor framework that integrates Fun.Css
See CHANGELOG.md for the patches included on top of upstream.
First, let`s check how it can look like:
style {
backgroundColor "#44c767"
borderRadius 30
borderWidth 1
borderStyleSolid
borderColor "#18ab29"
displayInlineBlock
cursorPointer
fontSize 17
}
Benchmarks (I know it is not fair comparison for Fss because Fss is more type safety and will automatically generate classname for you. But I did not find similar libraries to compare, just take as a reference), You can check the code in Benchmark/Benchmarks.fs:
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|---|---|---|---|---|---|
| BuildStyleWithFunCss | 181.2 ns | 2.33 ns | 2.18 ns | 0.0343 | 432 B |
| BuildStyleWithFunCssCustom | 170.9 ns | 2.31 ns | 2.05 ns | 0.0343 | 432 B |
| BuildStyleWithFeliz | 519.2 ns | 8.90 ns | 7.89 ns | 0.1593 | 2,000 B |
| BuildStyleWithFss | 6,042.3 ns | 65.63 ns | 61.39 ns | 0.8545 | 10,736 B |
This project is built in Fun.Blazor at first to help build inline style with type safety way.
Before I was using Feliz.Engine, when I was migrating Fun.Blazor to use InlineIfLambda for better performance, I found I can also make style building faster with the same way. So copied the Feliz.Engine basic methods for css and rebuild with computation plus InlineIfLambda.
The basic stuff is like this:
[<CustomOperation("color")>]
member inline _.color([<InlineIfLambda>] comb: CombineKeyValue, color: string) =
comb &>> ("color", color)
CombineKeyValue is defined as:
type CombineKeyValue = delegate of StringBuilder -> StringBuilder
So after you build with release mode, everything should combined in a local functions with a StringBuilder provide to append all the string pieces together.
How to use it in your project
It depends, take Fun.Blazor as an example, I will just inherit Fun.Css.CssBuilder and add a new Run member to generate the final result. In my case it is a AttrRenderFragment
type StyleBuilder() =
inherit Fun.Css.CssBuilder()
member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
AttrRenderFragment(fun _ builder index ->
let sb = stringBuilderPool.Get()
builder.AddAttribute(index, "style", combine.Invoke(sb).ToString())
stringBuilderPool.Return sb
index + 1
)
// With a helper function
let style = StyleBuilder()
Then I can use it in Fun.Blazor like this:
div {
style {
color "red"
height 100
width 100
}
}
Another example is just to generate a string for the style, then you can similar do things like:
type StyleStrBuilder() =
inherit Fun.Css.CssBuilder()
member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
let sb = stringBuilderPool.Get()
let str = combine.Invoke(sb).ToString()
stringBuilderPool.Return sb
str
// With a helper function
let styleStr = StyleStrBuilder()
For Fable + React, it does not support, because as what I know React is using an js object for the inline style. So the key value is not the pure css standard instead it use camelCase.
But you can use it in Fable to build pure css inline style string if you want.
How the bindings are built
The CSS surface in this fork is split across two files:
-
Fun.Css/CssBuilder.fs— hand-written: thestyle { ... }computation expression infrastructure (Builder,Yield,Run,Combine, operators), multi-arg shorthands (margin,padding,flex,gap,transform-origin,box-shadow, …), and CSS function-call value builders (transformMatrix,transformTranslate3D,filterBlur,*CubicBezier, …). Everything that is not a pure single-value property lives here. -
Fun.Css/CssBuilder.generated.fs— produced byscripts/generate.fsx. One(string)overload plus one typed-keyword op for every Baseline-supported CSS property pulled from W3C webref. Both files compile into the sameFun.Css.CssBuildertype surface.
Data sources
Pinned snapshots under data/css-spec/:
- webref — W3C's machine-readable CSS spec exports (
w3c/webref). One JSON file per spec module (css-fonts-4.json,css-text.json, etc.); 34 modules pinned today. - web-features — Baseline / browser-readiness signal (
web-platform-dx/web-features). Used to filter outBaseline=Limitedproperties unless explicitly opted-in.
Commands
# Run the test suite (Expecto + Hedgehog + a hand-rolled snapshot helper)
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi build.fsx -- -p test
# Regenerate Fun.Css/CssBuilder.generated.fs from the pinned data
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi scripts/generate.fsx
# Refresh pinned webref + web-features snapshots from upstream
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi scripts/refresh-data.fsx
# Benchmarks
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi build.fsx -- -p benchmark
Generator extension model
The generator runs from a small config in scripts/generate.fsx:
| Extension | Purpose |
|---|---|
| A | Universal *Initial / *InheritFromParent emission for every generated property. |
| B | Per-property int / float overloads (LengthPx adds a px suffix, BareNumber doesn't). |
| D | Per-property keyword override map (extraKeywords) for spec values hidden behind typed-references (<line-style>, <font-stretch-absolute>, <cursor-predefined>, etc.) that the shallow keyword extractor can't see through. |
| E | skipMainOverloads — for properties whose hand-written form has multi-arg overloads (margin, border-width, box-shadow, …), skip the colliding (string)/(int) main but still emit keyword + Ext A surface (distinct op names → no collision). |
| F | The same machinery as E, used to add net-new spec-canonical aliases (displayInline, wordBreak) alongside legacy hand-written names (displayInlineElement, wordbreak). |
| Filter override | allowedDespiteLimited — opt specific properties past the Baseline=Limited exclusion when the property's identity is universal and only narrower values carry the limit (e.g. cursor, background-attachment, text-justify, resize, user-select). |
Collisions between hand-written and generator output are handled by name: if the hand-written file already defines cursorAuto, the generator skips emitting its own cursorAuto. This lets the two surfaces coexist while migration is in progress.
Migration map
See LOGOS/upstream-pr-log.md for a per-commit log of what was migrated when, including PR-able boundaries. LOGOS/todos.md is the running task ledger.
TODO
[x] Add css selector, pseudo etc. (help wanted 😊)
But we may not need to build that, because it looks pretty complex and very flexible. Maybe we can just do this:
styleElement {
ruleset ".selected span:hover" {
color "red"
}
}
And it it generate things like
<style>
.selected span:hover {
color: red;
}
</style>
Even there is no type safety for the selector and pseudo class or element, but it is very straightforward to do.