Page
Library
Module
Module type
Parameter
Class
Class type
Source
Tw.VarSourceCSS 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.
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):
\@layer propertiesContains 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 themeContains theme design tokens - the actual values:
@layer theme {
:root, :host {
--font-weight-thin: 100;
--font-weight-bold: 700;
--text-xl: 1.25rem;
}
}@layer utilitiesContains 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 declarationsType 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%;
}@property RulesThe variable system has 4 constructors that map to clear, distinct behaviors:
1. theme - Design tokens in theme layer
@layer theme@property: Never generated--text-xl: 1.25rem referenced by .text-xl2. property_default - Variables with required initial values
@layer utilities@property: Always generated with initial-value--tw-border-style with initial solid3. channel ~needs_property:false - Composition variables
@layer utilities@property: Never generated--tw-translate-x, --tw-rotate combined by .transform4. channel ~needs_property:true - Animated composition variables
@layer utilities@property: Always generated without initial-value@property for animations--tw-font-weight, --tw-shadow-color5. ref_only - Reference-only variables
@property: Never generatedTo 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
~needs_property:true only if it needs animation supportThe 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.
Every variable must always have a concrete default value available for inline rendering:
@property initial value (* 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.
The variable system follows a simple, strict policy:
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
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_vFor 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.
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 StrategyUse @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) ]For override variables like border-style, utilities fall into two categories:
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) ]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.
Var.binding and pass results to childrenLayer classification for CSS variables
val property_info :
?initial:'a ->
?inherits:bool ->
?universal:bool ->
unit ->
'a property_infoproperty_info ?initial ?inherits ?universal () creates property metadata with defaults: inherits=false, universal=false.
The type for CSS variables with phantom type for role
type family = [ | `Border| `Rotate| `Skew| `Scale| `Translate| `Gradient| `Shadow| `Inset_shadow| `Ring| `Inset_ring| `Leading| `Font_weight| `Duration| `Tracking| `Content| `Text_shadow| `Filter| `Drop_shadow| `Backdrop_filter ]Theme variables (Pattern 1) - design tokens set in theme layer
Property default variables (Pattern 2) - variables with @property defaults
Reference-only variables (Pattern 4) - variables only referenced, never set
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).
val property_default :
'a Cascade.Css.kind ->
initial:'a ->
?inherits:bool ->
?universal:bool ->
?initial_css:string ->
?property_order:int ->
?family:family ->
string ->
'a property_defaultproperty_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.
val channel :
?needs_property:bool ->
?property_order:int ->
?family:family ->
'a Cascade.Css.kind ->
string ->
'a channelchannel ?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.
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.
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.
order name returns the theme layer order for a variable name. None if no order was set (i.e., not a theme variable).
family name returns the family for a variable name. None if the variable is not registered.
needs_property name returns whether a variable needs an \@property rule. Returns false if the variable is not registered or doesn't need \@property.
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.
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.
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.
val binding :
('a, [< `Theme | `Property_default | `Channel ]) t ->
?fallback:'a Cascade.Css.fallback ->
'a ->
Cascade.Css.declaration * 'a Cascade.Css.varbinding 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).reference var creates a reference to a variable. For ref_only and property_default variables only.
reference_with_fallback var fallback_value creates a variable reference with an explicit fallback value. Required for theme and channel variables.
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.
val reference_with_var_fallback :
('a, [< `Channel ]) t ->
('a, [< `Theme ]) t ->
'a ->
'a Cascade.Css.varreference_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.
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.emptyExample output:
\@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}The generated rule is emitted as CSS.
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 -> rto just:
let var =
Var.property_default Css.Color ~initial:(Css.Named Css.Black)
"tw-example-rules-short-color"
let property_rules = Var.property_rules varOnly use this for property_default variables where you expect a property rule.
properties vars generates deduplicated @property rules for all variables that need them, sorted deterministically by (name, kind).
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).
needs_property_rule v is true if v's underlying Var.t has property metadata.
order_of_declaration d returns theme ordering information for a custom declaration.
property_initial_string info converts the typed initial value of a @property declaration into a string suitable for initial-value:.
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.
bracket ?fallback name creates a reference to a user-supplied CSS variable from bracket notation. The name is the bare variable name without -- prefix.