Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/davesnx/styled-ppx/llms.txt

Use this file to discover all available pages before exploring further.

Interpolation lets you embed runtime values from your OCaml or ReScript code directly into a CSS string. The syntax is $(identifier) or $(Module.value) — placed wherever a CSS property value would go. styled-ppx reads the CSS property name surrounding each interpolation site and infers what type that position expects, so the compiler rejects a mismatched value before your code ever runs.

Syntax

Place $(...) inside any string passed to %styled.<tag>, %cx, or %css. The content must be a plain identifier ($(myVar)) or a module accessor ($(Module.value)) — arbitrary expressions are not supported:
let maxWidth = CSS.px(100)

module Theme = {
  let black = CSS.black
}

module Component = %styled.div(`
  border: 1px solid $(Theme.black);
  max-width: $(maxWidth);
`)
Don’t confuse this with ReScript/Reason’s built-in string interpolation (${} inside a template literal). styled-ppx’s $(...) lives inside a CSS string and is processed at compile time by the PPX, not by the language runtime.

How type inference works (type holes)

When styled-ppx encounters $(maxWidth) in the value position of max-width:, it checks the CSS grammar for max-width and determines that the value must be a length. It then constrains the type of maxWidth to the length polymorphic variant type. This is called type holes — the PPX punches a hole in the type and lets the compiler fill it in from context. The length type looks like this:
type length = [
  | #ch(float)
  | #em(float)
  | #ex(float)
  | #rem(float)
  | #vh(float)
  | #vw(float)
  | #vmin(float)
  | #vmax(float)
  | #px(int)
  | #pxFloat(float)
  | #cm(float)
  | #mm(float)
  | #inch(float)
  | #pc(float)
  | #pt(int)
  | #zero
  | #percent(float)
]
Because CSS.px(100) returns #px(int) which is a member of length, the compiler accepts it. Pass a string or a float directly and the compiler will report a type error.

Where interpolation is allowed

Interpolation works in three positions:
module Component = %styled.div(`
  border: 1px solid $(Theme.black); // ✅ property value
  @media $(Theme.small) { ... }     // ✅ media query prelude
  &:$(Theme.pseudo) { ... }         // ✅ selector
`)

Where interpolation is NOT allowed

You cannot interpolate into a property name. The property key must always be a literal CSS identifier:
// 🔴 Cannot interpolate into a property name
module X = %styled.div(`$(myProp): 10px`)
Unlike JavaScript-based solutions (e.g. styled-components `margin-${direction}: 10px`), styled-ppx requires property names to be static so the type checker can validate them at compile time.

Workaround: use the Array API with a switch/match

When you need to vary the CSS property itself based on a runtime value, enumerate every case explicitly using the Array API:
let margin = direction =>
  switch direction {
  | Left => %css("margin-left: 10px;")
  | Right => %css("margin-right: 10px;")
  | Top => %css("margin-top: 10px;")
  | Bottom => %css("margin-bottom: 10px;")
  }
Each branch produces a Css.rule value that can be placed inside a %styled or %cx array. See Array API for more.

Positional type inference in shorthand properties

For shorthand properties, styled-ppx infers the type of each interpolation from its position in the shorthand value list:
module Size = {
  let small = CSS.px(10)
}

%cx("margin: $(Size.small)")           // -> margin: 10px;
%cx("margin: $(Size.small) 0")         // -> margin: 10px 0;
%cx("margin: $(Size.small) $(Size.small)") // -> margin: 10px 10px;

The CSS module

All CSS value constructors are available through the CSS module provided by styled-ppx’s runtime library. Use CSS.px(10), CSS.hex("ff0000"), CSS.rem(1.5), CSS.pct(50.0), CSS.black, and so on to produce values that match the expected types. See Runtime: CSS for the full list of constructors.

A note on polymorphic shorthand properties

Some shorthand properties — background, animation, box-shadow, transition, transform — accept values whose types depend on the position and whether preceding values are present, and may have multiple valid overloads at each position. This makes it impossible for the type checker to resolve a unique type for an interpolation in those positions. For these properties, use the Array API and CSS.unsafe/2 as an escape hatch:
let randomProperty = CSS.unsafe("-webkit-invented-property", "10px")
let picture = %cx([randomProperty])

Not valid interpolation

The interpolation syntax only works inside a CSS string. You cannot pass a pre-built function reference or variable as the entire styled body:
// 🔴 Can't pass a function reference
let fn = (~kind, ~big) => { /* ... */ }
module X = %styled.div(fn)
// 🔴 Can't pass a variable reference as the body
let value = "display: block"
module X = %styled.div(value)
The PPX must see a string literal (or array literal) at that position in the AST to perform type inference. A reference is opaque to it at compile time.

Build docs developers (and LLMs) love