API ReferenceInterpolation

Interpolation

Interpolation is one of the most powerful and useful patterns in styled-ppx. You can use variables from your code to define the values of your styles. This is useful for reusing variables, handling themes, or conditionally applying styles based on props.

let maxWidth = CSS.px(100)
// CSS comes from the library called "styled-ppx" and it exposes all CSS values
// More information in the `Runtime: CSS` section
 
module Theme = {
  let black = CSS.black
}
 
module Component = %styled.div(`
  border: 1px solid $(Theme.black);
  max-width: $(maxWidth);
`)
let maxWidth = CSS.px(100);
// CSS comes from the library called "styled-ppx" and it exposes all CSS values
// More information in the `Runtime: CSS` section
 
module Theme = {
  let black = CSS.black;
};
 
module Component = [%styled.div
  {|
  border: 1px solid $(Theme.black);
  max-width: $(maxWidth);
|}
];
💡

Don’t confuse interpolation from String Interpolation from ReScript

Features

  • Type-safety via type holes
  • Support for properties
  • Support for shorthand properties
  • Support for selectors
  • Support for media queries

Rules

Interpolation allows you to use any identifier as a value inside CSS, where “any identifier” refers to any runtime variables or module accessors. Arbitrary expressions are not supported, yet. More information here.

Interpolation works inside property values, media-queries and selectors. The rules for interpolation works the same way as CSS variables (var()).

module Component = %styled.div(`
  border: 1px solid $(Theme.black); // ✅
  @media $(Theme.small) { ... } // ✅
  &:$(Theme.small) { ... } // ✅
`)
module Component = [%styled.div {|
  border: 1px solid $(Theme.black); // ✅
  @media $(Theme.small) { ... } // ✅
  &:$(Theme.small) { ... } // ✅
|}];

It’s not allowed to interpolate on entire properties, neither any other interpolation. Which might be slightly different from other SASS/Less and JavaScript-based solutions (such as styled-components or emotion) as their interpolation is more dynamic and can be used everywhere.

styled-ppx forces you to be more rigid but with the promise of being always type-safe, which is the same way as ReScript does ❤️. The dynamism from JavaScript-based solutions comes with the cost of being unsafe.

Here’s a valid JavaScript example `margin-${whatever}: 10px` from styled-components. Which isn’t valid in styled-ppx with the same syntax. As explained above, this interpolation can’t be applied to entire properties or half-properties.

The solution is simple, you would handle all properties based on the dynamic value:

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;")
  }
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;"]
  };

If you aren’t familiar with %css [%css] extension, take a look at the %css [%css] section.

Example

Any value from any property can be interpolated. It relies on the position of the interpolation to guess which value you are trying to interpolate.

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;
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;

Type-safety

The example above introduces the API from CSS to define the value of margin. We expect you to use it to make sure the values are interpoilated with the right type. In the example above margin expects one of:

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)
]
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)
];

Since Size.small Size.small is #px(int) `px(int), the type-checker would allow it.

A note about the polymorphism of CSS

There are plenty of shorthand properties in CSS that accept different values that depend on the position to have a certain meaning. Making it challenging for the type-checker to know which value to use. Some of the most problematic ones are animation, box-shadow/text-shadow, background, transition and transform, to name a few.

These properties are not challenging to type because they are shorthand properties, but because they have values that are positional and optional at the same time.

Let’s look at background.

background: #fff; /* The background is white */
background: url(img.png); /* The background is an image */
background: #fff url(img.png); /* The background is white with an image */
background: url(img.png) no-repeat; /* The background is a non-repeating image */

In this case, to interpolate the background’s value like: background: $(variable1) $(variable2) the type-checker can’t know the type of $(variable1) and (variable2) ahead of time, because there are numerous possibilities for a valid background CSS property. This is called an overload in other languages and it’s not available in ReScript due to it’s nature of being a static typed language.

What if a property isn’t supported

First, if you have the time, please open an issue. Most properties are trivial to add support for. If time isn’t your best friend then there’s a workaround.

There is no way to add unsafe behaviour to CSS definitions. We prefer to keep improving the overall safety of styled-ppx via requests/issues rather than allowing a method for unsafe functionality - doing so would negate the whole purpose of styled-ppx.

The workaround is to use the Array API to generate %cx [%cx] calls. For example:

let block: Css.rule = %css("display: block")
let randomProperty = CSS.unsafe("-webkit-invented-property", "10px");
let picture = %cx([block, randomProperty]);
let block: Css.rule = [%css "display: block"];
let randomProperty = CSS.unsafe("-webkit-invented-property", "10px");
let picture = [%cx [|block, randomProperty|]];
let randomProperty = CSS.unsafe("-webkit-invented-property", "10px");
let picture = %styled.div([randomProperty]);
let randomProperty = CSS.unsafe("-webkit-invented-property", "10px");
let picture = [%styled.div [|randomProperty|]];

Here the safety will rely on your usage of CSS.unsafe CSS.unsafe correctly.

For a general overview of the list take a look at support for CSS.

Not valid interpolation

Interpolation in a ppx is a little limited, which makes a few “use-cases” not possible, for example: abstract a function or a variable reference.

// 🔴 Can't pass a function
let fn = (~kind, ~big) => { /* ... */ };
module X = %styled.div(fn)
/* 🔴 Can't pass a function*/
let fn = (~kind, ~big) => /* ... */
module X = [%styled.div fn];
// 🔴 Can't pass a variable reference
let value = "display: block"
module X = %styled.div(value)
/* 🔴 Can't pass a variable reference*/
let value = "display: block";
module X = [%styled.div value];