Logo FS-Skia-UI

The design-token flow

This literate script is evaluated at documentation-build time (fsdocs build --eval), so it is compiler-verified against the real FS.Skia.UI.Controls design-token surface.

Design tokens have a single source of truth: the DTCG file src/Controls/design-tokens.tokens.json (authored from the Penpot / DTCG design source). From it, a generated F# module DesignTokens (with Light and Dark sub-modules) is produced, and the DesignTokenDrift gate keeps the generated F# in lock-step with the JSON. Themes are built from those typed primitives, and the control suite reads the theme at render time. This script walks that flow from the typed token values outward — no design tooling or GPU required.

#I "../../src/Controls/bin/Debug/net10.0"
#r "FS.Skia.UI.Scene.dll"
#r "FS.Skia.UI.Layout.dll"
#r "FS.Skia.UI.Controls.dll"

open FS.Skia.UI.Controls

Typed token primitives

DesignTokens.Light / DesignTokens.Dark are compiler-checked values generated from the DTCG source. Because they are real F# values, a typo or a removed token is a compile error, and references to them are greppable.

printfn "light foreground : %A" DesignTokens.Light.foreground
printfn "light background : %A" DesignTokens.Light.background
printfn "light accent     : %A" DesignTokens.Light.accent
printfn "dark  accent     : %A" DesignTokens.Dark.accent

Some dark-theme tokens are aliases of their light counterparts in the DTCG source (e.g. dark.danger references light.danger); the generator resolves the alias so both evaluate to the same value:

printfn "light danger     : %A" DesignTokens.Light.danger
printfn "dark  danger     : %A" DesignTokens.Dark.danger
printfn "danger aliased?  : %b" (DesignTokens.Light.danger = DesignTokens.Dark.danger)

Non-colour primitives (sizing, density, contrast) flow through the same source:

printfn "font size        : %.1f" DesignTokens.Light.fontSize
printfn "density          : %.1f" DesignTokens.Light.density
printfn "corner radius    : %.1f" DesignTokens.Light.cornerRadius
printfn "min contrast     : %.2f" DesignTokens.Light.contrastRequiredRatio

From tokens to a theme

Theme.light / Theme.dark are assembled from these primitives, and the Theme.with* combinators let an app override individual tokens while keeping the rest. Here we take the light theme and swap in the dark accent token — exactly how a consumer applies a brand colour sourced from the design system:

let brandTheme = Theme.light |> Theme.withAccent DesignTokens.Dark.accent

The control suite reads the resolved Theme at render time, so every control picks up the token values without per-control wiring. To change the palette, edit the DTCG JSON and regenerate (./fake.sh build -t RefreshSurfaceBaselines); DesignTokenDrift fails the build if the generated F# and the JSON disagree.

See the design-token / Penpot deep dive, the typed front door, and the API reference.

namespace FS
namespace FS.Skia
namespace FS.Skia.UI
namespace FS.Skia.UI.Controls
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
module DesignTokens from FS.Skia.UI.Controls
<summary> Typed, compiler-checked design-token values generated from `src/Controls/design-tokens.tokens.json` (the DTCG single source of truth). Token VALUES are generated; this curated signature is the sole public-surface declaration. Token references are greppable and stay in lock-step with the DTCG source via DesignTokenDrift. </summary>
module Light from FS.Skia.UI.Controls.DesignTokens
<summary> Light-theme primitives (feed Theme.light; value-identical to the pre-feature literals). </summary>
val foreground: FS.Skia.UI.Scene.Color
<summary> Light-theme primary foreground (text/icon) colour. </summary>
val background: FS.Skia.UI.Scene.Color
<summary> Light-theme surface/background colour. </summary>
val accent: FS.Skia.UI.Scene.Color
<summary> Light-theme accent colour for primary/active emphasis. </summary>
module Dark from FS.Skia.UI.Controls.DesignTokens
<summary> Dark-theme primitives (feed Theme.dark; value-identical to the pre-feature literals). </summary>
val accent: FS.Skia.UI.Scene.Color
<summary> Dark-theme accent colour for primary/active emphasis. </summary>
val danger: FS.Skia.UI.Scene.Color
<summary> Light-theme danger/destructive colour for errors and destructive actions. </summary>
val danger: FS.Skia.UI.Scene.Color
<summary> Dark-theme danger/destructive colour (aliases the light-theme danger token in the DTCG source). </summary>
val fontSize: float
<summary> Light-theme base font size in device-independent units. </summary>
val density: float
<summary> Light-theme density multiplier scaling spacing and control sizing. </summary>
val cornerRadius: float
<summary> Light-theme default corner radius for rounded control surfaces. </summary>
val contrastRequiredRatio: float
<summary> Minimum foreground/background contrast ratio the light theme must satisfy. </summary>
val brandTheme: Theme
Multiple items
union case AttrCategory.Theme: AttrCategory

--------------------
module Theme from FS.Skia.UI.Controls

--------------------
type Theme = { Name: string Foreground: Color Background: Color Accent: Color Danger: Color Muted: Color FontFamily: string option FontSize: float Density: float CornerRadius: float ... }
<summary> Design-token palette and metrics (`Theme`): the named color roles (`Foreground`/`Background`/`Accent`/`Danger`/`Muted`), typography (`FontFamily`/`FontSize`), and layout metrics (`Density`/`CornerRadius`/`ContrastRequiredRatio`). </summary>
val light: Theme
<summary> The built-in light `Theme` (DTCG `DesignTokens.Light` palette). </summary>
val withAccent: accent: FS.Skia.UI.Scene.Color -> theme: Theme -> Theme
<summary> Return `theme` with its accent colour replaced by `accent`. </summary>

Type something to start searching.