package tw

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Module Tw.VarSource

CSS variable tracking and generation

Typed CSS variable definitions and CSS generation architecture.

This module provides a type-safe system for CSS custom properties that generates CSS across multiple layers following Tailwind v4's architecture.

CSS Output Architecture

The variable system generates CSS across multiple layers following Tailwind v4's architecture. Each layer has its own deterministic ordering rules:

Layer Ordering Rules (Tailwind v4, mirrored by tw):

  • Theme layer: Order is stable and intentional, not raw alphabetical. Tokens appear in a canonical sequence (e.g., default font families, then color palette in palette order, then scales like radius/shadow/spacing, etc.). In this repo, theme order is explicitly sorted by metadata (priority, subindex) attached to each variable; lower priority first, then subindex.
  • Properties layer: Split into two parts for parity with Tailwind's output shape:
  • Properties layer (near top): Emits initial values for custom properties inside a guarded @supports block (applies to "*, ::before, ::after, ::backdrop"). Ordering follows the order of corresponding @property rules.
  • @property rules (at end): Appended at the very end of stylesheet (after all layers), in first-seen order: explicit rules from utilities first, then auto-generated ones for variables flagged as "needs @property".
  • Utilities layer: Ordered by "conflict resolution" groups to ensure predictable cascade. Utilities are grouped and sorted by group priority, then by per-group suborder. Examples: display → position → margin → background → padding → typography → border → sizing → effects → interactivity → flexbox/grid → gap → container/prose. Within background colors, sort uses canonical palette order; for margins/padding, "all" precedes axis which precedes side-specific.

\@layer properties

Contains initial values for utility variables that need \@property registration:

  @layer properties {
    *, :before, :after, ::backdrop {
      --tw-shadow: 0 0 #0000;
      --tw-border-style: initial;
      --tw-shadow-alpha: 100%;
    }
  }

@layer theme

Contains theme design tokens - the actual values:

  @layer theme {
    :root, :host {
      --font-weight-thin: 100;
      --font-weight-bold: 700;
      --text-xl: 1.25rem;
    }
  }

@layer utilities

Contains utility class definitions that set variables and CSS properties:

  @layer utilities {
    .font-thin {
      --tw-font-weight: var(--font-weight-thin);
      font-weight: var(--font-weight-thin);
    }
    .border-solid {
      --tw-border-style: solid;
      border-style: solid;
    }
    .border {
      border-style: var(--tw-border-style);
      border-width: 1px;
    }
  }

@property declarations

Type registrations for animated/transitionable variables (at the end):

  \@property --tw-shadow {
    syntax: "*";
    inherits: false;
    initial-value: 0 0 #0000;
  }
  \@property --tw-shadow-alpha {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 100%;
  }

Variable Types and @property Rules

The variable system has 4 constructors that map to clear, distinct behaviors:

Constructor Rules

1. theme - Design tokens in theme layer

  • Layer: @layer theme
  • @property: Never generated
  • Usage: Utilities reference these values but never modify them
  • Example: --text-xl: 1.25rem referenced by .text-xl

2. property_default - Variables with required initial values

  • Layer: @layer utilities
  • @property: Always generated with initial-value
  • Usage: Some utilities set it, others rely on the default
  • Example: --tw-border-style with initial solid

3. channel ~needs_property:false - Composition variables

  • Layer: @layer utilities
  • @property: Never generated
  • Usage: Multiple utilities set portions, aggregator combines them
  • Example: --tw-translate-x, --tw-rotate combined by .transform

4. channel ~needs_property:true - Animated composition variables

  • Layer: @layer utilities
  • @property: Always generated without initial-value
  • Usage: Same as channel but needs @property for animations
  • Example: --tw-font-weight, --tw-shadow-color

5. ref_only - Reference-only variables

  • Layer: None (no declaration)
  • @property: Never generated
  • Usage: Only referenced with explicit fallback, never set by our utilities
  • Example: Variables from other libraries we reference but don't control

Simple Decision Tree

To choose the right constructor:

1. Is it a design token shared across utilities? → theme 2. Does it need a default value for referencing utilities? → property_default 3. Is it only referenced, never set by us? → ref_only 4. Otherwise it's a utility variable → channel

  • Add ~needs_property:true only if it needs animation support

Inline Mode Requirements

The variable system must support an inline mode that generates CSS without any custom properties, suitable for embedding in HTML style attributes or environments that don't support CSS variables.

Inline Mode Constraint

Every variable must always have a concrete default value available for inline rendering:

  • Theme variables: use the stored theme value
  • Property_default variables: use the @property initial value
  • Channel variables: use the identity/zero value (0px, 0deg, 1.0, etc.)
  • Ref_only variables: use the fallback value
  • Always-set variables: use the bound value

Example: Variables vs Inline Mode

  (* Variables mode *)

  .border { border-style: var(--tw-border-style); border-width: 1px; }
  .border-solid { --tw-border-style: solid; border-style: solid; }

  (* Inline mode - no variables *)
  .border { border-style: solid; border-width: 1px; }  (* uses [@property] initial *)
  .border-solid { border-style: solid; }

This constraint ensures the same CSS classes work identically whether variables are supported or not, enabling progressive enhancement and broader compatibility.

Variable Usage Policy

The variable system follows a simple, strict policy:

The Three Rules

1. When you need both declaration and variable reference: Use Var.binding 2. When you need only declaration OR only variable reference: Pass it as function parameter, let the parent function call Var.binding 3. No other ways are allowed: No direct Css.var_ref, no ignoring declarations, no workarounds

Examples

Rule 1: Need both declaration and variable

let my_var = Var.theme Css.Color "example-color" ~order:(999, 0)

let my_utility =
  let var_d, var_v = Var.binding my_var (Css.Named Css.Red) in
  Style.style [ var_d; Css.color (Css.Var var_v) ]

Rule 2: Need only declaration OR only variable reference

let color_utility var_ref = Style.style [ Css.color (Css.Var var_ref) ]

let declaration_utility var_decl =
  Style.style [ var_decl; Css.color (Css.Named Css.Black) ]

let my_var = Var.theme Css.Color "example-parent-color" ~order:(999, 1)
let value : Css.color = Css.Named Css.Blue

let parent_utility =
  let var_d, var_v = Var.binding my_var value in
  let _set_only = declaration_utility var_d in
  color_utility var_v

Advanced Patterns

Theme Record Pattern

For complex utilities involving multiple variables, use a record to manage all bindings centrally:

type text_theme = {
  color : Css.declaration * Css.color Css.var;
  size : Css.declaration * Css.length Css.var;
}

let color_var = Var.theme Css.Color "example-text-color" ~order:(999, 2)
let size_var = Var.theme Css.Length "example-text-size" ~order:(999, 3)

let default_text_theme =
  let color_d, color_v = Var.binding color_var (Css.Named Css.Black) in
  let size_d, size_v = Var.binding size_var (Css.Rem 1.) in
  { color = (color_d, color_v); size = (size_d, size_v) }

let text_utility size_value =
  let theme = default_text_theme in
  let new_size_d, new_size_v = Var.binding size_var size_value in
  let updated_theme = { theme with size = (new_size_d, new_size_v) } in
  Style.style
    [
      fst updated_theme.size;
      Css.font_size (Css.Var (snd updated_theme.size));
      Css.color (Css.Var (snd theme.color));
    ]

This pattern allows selective variable updates while maintaining consistent defaults for other variables in the group.

Inline Mode Semantics

The goal is to make inline mode work properly by ensuring every variable has a clear, single point where it's defined and set. This is achieved by always following the three rules unless no declaration exists in the Tailwind case.

The binding function's value parameter serves as the default for inline mode:

let color_var = Var.theme Css.Color "example-inline-color" ~order:(999, 4)
let color_decl, color_ref = Var.binding color_var (Css.Named Css.Red)
let text_red = Style.style [ color_decl; Css.color (Css.Var color_ref) ]

This guarantees consistent behavior between Variables and Inline modes with a single source of truth for each variable's value.

@Property Registration Strategy

Use @property registration for variables that need type safety, animation support, or fallback defaults for referencing utilities:

let example_color =
  Var.property_default Css.Color ~initial:(Css.Named Css.Black)
    "tw-example-property-color"

let set_color =
  let decl, var_ref = Var.binding example_color (Css.Named Css.Red) in
  Style.style [ decl; Css.color (Css.Var var_ref) ]

let reference_color =
  let var_ref = Var.reference example_color in
  Style.style
    ~property_rules:(Var.property_rules example_color)
    [ Css.color (Css.Var var_ref) ]

The Border Pattern: Setting vs Referencing Variables

For override variables like border-style, utilities fall into two categories:

Setting Utilities

Set the variable to a specific value (use Var.binding):

let border_style_var =
  Var.property_default Css.Border_style ~initial:Css.Solid
    "tw-example-border-style"

let border_solid =
  let decl, var_ref = Var.binding border_style_var Css.Solid in
  Style.style [ decl; Css.border_style (Css.Var var_ref) ]

Referencing Utilities

Reference the variable with @property default (use Var.reference + ~property_rules):

let border_style_var =
  Var.property_default Css.Border_style ~initial:Css.Solid
    "tw-example-border-style-reference"

let border =
  let var_ref = Var.reference border_style_var in
  Style.style
    ~property_rules:(Var.property_rules border_style_var)
    [ Css.border_style (Css.Var var_ref); Css.border_width (Css.Px 1.) ]

This pattern ensures @property rules are only generated when utilities actually need the fallback defaults, not when they set the variable.

Module Organization

  • Variables are declared at module top
  • Each variable is owned by one module
  • Cross-module usage only via function parameters
  • Parent functions call Var.binding and pass results to children
Sourcetype layer =
  1. | Theme
  2. | Utility

Layer classification for CSS variables

Sourcetype 'a property_info = {
  1. initial : 'a option;
  2. inherits : bool;
  3. universal : bool;
}

Property metadata for @property registration.

  • initial: Optional initial value. If None, properties layer uses "initial"
  • inherits: Whether the property inherits from parent elements
  • universal: Force universal syntax "*" instead of typed syntax
Sourceval property_info : ?initial:'a -> ?inherits:bool -> ?universal:bool -> unit -> 'a property_info

property_info ?initial ?inherits ?universal () creates property metadata with defaults: inherits=false, universal=false.

Sourcetype ('a, 'r) t

The type for CSS variables with phantom type for role

Sourceval pp : ('a, 'r) t -> string

pp v returns a string representation of a variable.

Type shortcuts for common patterns

Sourcetype family = [
  1. | `Border
  2. | `Rotate
  3. | `Skew
  4. | `Scale
  5. | `Translate
  6. | `Gradient
  7. | `Shadow
  8. | `Inset_shadow
  9. | `Ring
  10. | `Inset_ring
  11. | `Leading
  12. | `Font_weight
  13. | `Duration
  14. | `Tracking
  15. | `Content
  16. | `Text_shadow
  17. | `Filter
  18. | `Drop_shadow
  19. | `Backdrop_filter
]
Sourcetype 'a theme = ('a, [ `Theme ]) t

Theme variables (Pattern 1) - design tokens set in theme layer

Sourcetype 'a property_default = ('a, [ `Property_default ]) t

Property default variables (Pattern 2) - variables with @property defaults

Sourcetype 'a channel = ('a, [ `Channel ]) t

Channel variables (Pattern 3) - composition variables

Sourcetype 'a ref_only = ('a, [ `Ref_only ]) t

Reference-only variables (Pattern 4) - variables only referenced, never set

Core API

Sourceval theme : 'a Cascade.Css.kind -> ?runtime:bool -> string -> order:(int * int) -> 'a theme

theme kind name ?runtime ~order creates a Theme-layer variable (design token). Values are set via Var.binding at use sites that own the declaration. Enforces explicit ordering for deterministic theme output.

With ~runtime:true the optimizer keeps var(name) references unfolded instead of inlining the theme value, so the token stays overridable at runtime (matching Tailwind, which leaves e.g. calc(var(--spacing)*4) intact rather than baking in the resolved length).

Sourceval property_default : 'a Cascade.Css.kind -> initial:'a -> ?inherits:bool -> ?universal:bool -> ?initial_css:string -> ?property_order:int -> ?family:family -> string -> 'a property_default

property_default kind ~initial name ?inherits ?universal ?property_order creates a Utility variable with a typed \@property registration and an initial value used for referencing utilities and inline mode. The initial value is required for proper \@property registration and reference fallbacks.

property_order specifies the ordering of this variable in the @layer properties @supports block. Lower values appear first.

IMPORTANT: Due to current architecture limitations, any style that uses a property_default variable MUST include its property_rule explicitly:

let my_var =
  Var.property_default Css.Content ~initial:(Css.String "")
    "tw-example-content"

let my_style =
  let decl, ref_ = Var.binding my_var (Css.String "") in
  let property_rules =
    match Var.property_rule my_var with None -> Css.empty | Some r -> r
  in
  Style.style ~property_rules [ decl; Css.content (Css.Var ref_) ]

This ensures the \@property rule with the correct initial value flows through the system. Without this, a generic \@property rule without initial value may be generated, breaking the CSS output.

TODO: Fix this architecture limitation to automatically include property rules for property_default variables in rules.ml without requiring explicit inclusion.

Sourceval channel : ?needs_property:bool -> ?property_order:int -> ?family:family -> 'a Cascade.Css.kind -> string -> 'a channel

channel ?needs_property ?property_order kind name creates a Utility variable. When needs_property is true, generates an \@property rule for animation support. property_order specifies ordering in the @supports block. Ideal for composition patterns where contributing utilities set declarations and aggregators reference values.

Sourceval property_order : string -> int option

property_order name returns the property order for a variable name, used for sorting properties in the @layer properties @supports block. Returns None if no order was registered.

Sourceval register_property_order : name:string -> order:int -> unit

register_property_order ~name ~order registers a property order for a variable name. Used by modules that create \@property rules directly (without using Var.property_default) and need to participate in the properties layer ordering. The name should be without the "--" prefix.

Sourceval order : string -> (int * int) option

order name returns the theme layer order for a variable name. None if no order was set (i.e., not a theme variable).

Sourceval family : string -> family option

family name returns the family for a variable name. None if the variable is not registered.

Sourceval needs_property : string -> bool

needs_property name returns whether a variable needs an \@property rule. Returns false if the variable is not registered or doesn't need \@property.

Sourceval ref_only : 'a Cascade.Css.kind -> string -> fallback:'a -> 'a ref_only

ref_only kind name ~fallback creates a reference-only handle to a Utility variable with a concrete fallback for inline mode. No declaration is produced. This implements Pattern 4 - variables that are only referenced, never set.

Sourceval theme_ref : ?default:'a -> ?default_css:string -> string -> 'a Cascade.Css.var

theme_ref ?default ?default_css name creates a bare var reference to a theme variable. In variables mode, emits var(--name).

When default and default_css are provided, the variable is registered in the resolution registry: when the theme doesn't define it, the concrete default_css string is emitted instead. default is the typed value used for inline mode.

When default and default_css are omitted, the var reference is created without resolution registration. Use this for dynamically-constructed theme variable names (e.g., blur-xl, spacing-4) where the theme value is already registered elsewhere.

Sourceval resolve_theme_refs : string -> string option

resolve_theme_refs name returns the default CSS string value for a theme_ref variable. Used as theme_defaults in Pp.ctx when theme variables don't exist and should be replaced by their concrete defaults.

Sourceval binding : ('a, [< `Theme | `Property_default | `Channel ]) t -> ?fallback:'a Cascade.Css.fallback -> 'a -> Cascade.Css.declaration * 'a Cascade.Css.var

binding var ?fallback value creates both a CSS declaration and a var() reference with the value as default for inline mode. This is the primary way to use variables.

  • fallback if provided, the var reference will use this fallback instead of the default value. Can be Empty for var(--name,), None for var(--name), or Fallback value for var(--name, value). This is useful for utilities that want to reference a variable with a different fallback (e.g., text-xs references --tw-leading with --text-xs--line-height as fallback).
Sourceval reference : ('a, [< `Ref_only | `Property_default ]) t -> 'a Cascade.Css.var

reference var creates a reference to a variable. For ref_only and property_default variables only.

Sourceval reference_with_fallback : ('a, [< `Theme | `Channel ]) t -> 'a -> 'a Cascade.Css.var

reference_with_fallback var fallback_value creates a variable reference with an explicit fallback value. Required for theme and channel variables.

Sourceval reference_with_empty_fallback : ('a, [< `Channel ]) t -> 'a Cascade.Css.var

reference_with_empty_fallback var creates a variable reference with an empty fallback, producing var(--name,). Used for optional transform components where unset variables should contribute nothing.

Sourceval reference_with_var_fallback : ('a, [< `Channel ]) t -> ('a, [< `Theme ]) t -> 'a -> 'a Cascade.Css.var

reference_with_var_fallback channel_var theme_var dummy_value creates a variable reference to channel_var with a nested var fallback to theme_var. Produces: var(--channel, var(--theme-fallback)). The dummy_value is used for type inference but not in the output.

Sourceval property_rule : ('a, [< `Property_default | `Channel ]) t -> Cascade.Css.t option

property_rule var generates the @property rule if metadata is present. Returns None if the variable has no property metadata.

Used with Var.reference to provide explicit property rules:

let my_var =
  Var.property_default Css.Color ~initial:(Css.Named Css.Black)
    "tw-example-rule-color"

let property_rules =
  match Var.property_rule my_var with
  | Some rule -> rule
  | None -> Css.empty

Example output:

  \@property --tw-shadow {
    syntax: "*";
    inherits: false;
    initial-value: 0 0 #0000;
  }

The generated rule is emitted as CSS.

Sourceval property_rules : ('a, [< `Property_default ]) t -> Cascade.Css.t

property_rules var is a convenience function for property_default variables that returns either the property rule or Css.empty if there is none. This simplifies the common pattern of:

let var =
  Var.property_default Css.Color ~initial:(Css.Named Css.Black)
    "tw-example-rules-color"

let property_rules =
  match Var.property_rule var with None -> Css.empty | Some r -> r

to just:

let var =
  Var.property_default Css.Color ~initial:(Css.Named Css.Black)
    "tw-example-rules-short-color"

let property_rules = Var.property_rules var

Only use this for property_default variables where you expect a property rule.

Heterogeneous Collections

Sourcetype any_var =
  1. | Any : ('a, 'r) t -> any_var
    (*

    Existential type for heterogeneous collections of variables

    *)
Sourceval properties : any_var list -> Cascade.Css.t

properties vars generates deduplicated @property rules for all variables that need them, sorted deterministically by (name, kind).

Helper Types and Functions

Sourceval css_name : ('a, _) t -> string

css_name var returns the full CSS property name with -- prefix. For example, css_name gradient_from_var returns "--tw-gradient-from". Use this when you need the property name (e.g., for transition-property).

Sourceval needs_property_rule : 'a Cascade.Css.var -> bool

needs_property_rule v is true if v's underlying Var.t has property metadata.

Sourceval order_of_declaration : Cascade.Css.declaration -> (int * int) option

order_of_declaration d returns theme ordering information for a custom declaration.

Sourceval property_initial_string : Cascade.Css.property_info -> string

property_initial_string info converts the typed initial value of a @property declaration into a string suitable for initial-value:.

Bracket Variable References

For user-supplied CSS variable names from bracket notation like bg-[var(--my-color)]. These are variables whose names come from user input, not from our typed variable system.

Sourceval bracket : ?fallback:'a Cascade.Css.fallback -> string -> 'a Cascade.Css.var

bracket ?fallback name creates a reference to a user-supplied CSS variable from bracket notation. The name is the bare variable name without -- prefix.