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.

Dynamic components let you drive CSS values directly from React props. Instead of a static CSS string, you provide a function — the labelled arguments become props on the generated component, and the function body is a CSS string that interpolates those arguments. styled-ppx validates the CSS at compile time and infers the correct type for each argument based on where it appears in the CSS.

Function form of %styled.<tag>

Wrap your CSS string in a function that takes labelled arguments (~argName) and returns the CSS string. styled-ppx picks up the argument names, exposes them as component props, and infers their types from the CSS property positions they are used in — a technique called type holes.
module Dynamic = %styled.div((~color, ~background) => `
  color: $(color);
  background-color: $(background);
`)

<Dynamic color=CSS.hex("#EB5757") background=CSS.hex("#516CF0")>
  {React.string("Hello!")}
</Dynamic>
In the example above color and background are inferred as CSS.Color.t because they appear in the color and background-color CSS properties, which accept colour values. You can confirm this by hovering the prop in your editor — no annotation is required. All interpolation rules apply inside the function body.

How type inference works (type holes)

When styled-ppx sees $(color) in the position of a CSS property value, it reads the property name (color:) and uses the known CSS grammar to determine what type that position accepts. It then unifies that type with the labelled argument ~color, giving it a concrete type without any manual annotation. This means:
  • margin: $(size)size is inferred as a CSS length type (e.g. CSS.px(int), CSS.rem(float), etc.)
  • color: $(c)c is inferred as CSS.Color.t
  • opacity: $(o)o is inferred as float
If you pass a value of the wrong type the compiler will report a type error at the call site, not inside a generated file.

Default prop values

Labelled arguments can have default values using the standard OCaml/ReScript default argument syntax:
module Align = %styled.div((~distribute=#Center, ~align=#Center) => [
  %css("display: flex"),
  %css("height: 100%"),
  switch distribute {
  | #Start => %css("justify-content: flex-start")
  | #Center => %css("justify-content: center")
  | #End => %css("justify-content: flex-end")
  },
  switch align {
  | #Start => %css("align-items: flex-start")
  | #Center => %css("align-items: center")
  | #End => %css("align-items: flex-end")
  },
])

<Align distribute=#Start align=#Start />

Important limitation: the returned expression must be a literal

The last expression of a dynamic component function — the value that becomes the CSS — must be a string literal or an array literal written directly at that position. It cannot be a variable reference or a function call.
These patterns will not compile:
// 🔴 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 inspect the literal AST node at compile time to determine which CSS types to infer. A reference is opaque to the PPX.
If you need complex logic before producing the CSS, use a function body (braces + let bindings) that ends with an array literal — the Array API handles this case perfectly:
module Button = %styled.button((~variant) => {
  let color = Theme.button(~variant)
  [
    %css("background-color: $(color)"),
    %css("width: 100%;"),
  ]
})

Name collision with DOM props

All styled components expose the standard DOM props for their element via makeProps. If you declare a dynamic prop with the same name as one of those DOM props, the dynamic prop wins — the original DOM prop is replaced.
The prop name size exists on several HTML elements (e.g. <input size>). If you define a dynamic component with ~size, the standard DOM size prop is replaced by your typed CSS size prop.Pick prop names that don’t clash with native DOM attributes to avoid surprising behaviour. See ReactDOM.res for the full list of DOM props.

Using the Array API for more complex logic

When you need pattern matching, conditional logic, or variable bindings inside a dynamic component, combine it with the Array API. Return an array of Css.rule values from your function instead of a plain string. See Array API for full examples including switch expressions and runtime theme lookups.

Build docs developers (and LLMs) love