Skip to content

Styling with CSS

The restaurant website’s order confirmation widget is far from complete, but Madame Jellobutter insists that you make the widget less ugly before you do anything else. In previous chapters, we saw how to add styles using the style prop, but let’s see how to do it with good old CSS.

Add the first CSS file

Add a new file src/order-confirmation/order-item.css and give it these styles:

css
.item {
  border-top: 1px solid lightgray;
}

.emoji {
  font-size: 2em;
}

.price {
  text-align: right;
}
.item {
  border-top: 1px solid lightgray;
}

.emoji {
  font-size: 2em;
}

.price {
  text-align: right;
}

Import using mel.raw

In OCaml, there is no syntax to import from files, because all modules within a project are visible to all other modules[1]. However, we can make use of JavaScript’s import syntax by using the mel.raw extension node, which allows us to embed raw JavaScript in our OCaml code. Add the following line to the top of Order.re:

reason
[%%mel.raw {|import "./order-item.css"|}];
[%%mel.raw {|import "./order-item.css"|}];

The {||} string literal is known as a quoted string literal, and it is used to represent strings of arbitrary content without escaping[2]. They are similar to the {js||js} string literals we first saw in the Celsius Converter chapter, with the difference that they won’t handle Unicode correctly.

Unfortunately, in the terminal where we’re running npm run serve, we see this Vite compilation error:

6:05:39 PM [vite] Pre-transform error:
Failed to load url /_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css
(resolved id:
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css)
in
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/Order.js.
Does the file exist?
6:05:39 PM [vite] Pre-transform error:
Failed to load url /_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css
(resolved id:
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css)
in
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/Order.js.
Does the file exist?

Tell Dune to copy CSS files

The problem is that Vite is serving the app from the build directory at _build/default/src/order-confirmation/output/src/order-confirmation, and the order-item.css file isn’t in that build directory.

To solve this, we can add the runtime_deps field to our melange.emit stanza in src/order-confirmation/dune:

dune
(melange.emit
 (target output)
 (libraries reason-react)
 (preprocess
  (pps melange.ppx reason-react-ppx))
 (module_systems es6)
 (runtime_deps order-item.css))
(melange.emit
 (target output)
 (libraries reason-react)
 (preprocess
  (pps melange.ppx reason-react-ppx))
 (module_systems es6)
 (runtime_deps order-item.css))

We also want to add styles for the Order component, so add a new file src/order-confirmation/order.css with these styles:

css
table.order {
  border-collapse: collapse;
}

table.order td {
  padding: 0.5em;
}

.total {
  border-top: 1px solid gray;
  font-weight: bold;
  text-align: right;
}
table.order {
  border-collapse: collapse;
}

table.order td {
  padding: 0.5em;
}

.total {
  border-top: 1px solid gray;
  font-weight: bold;
  text-align: right;
}

To ensure that order.css is also copied to the build directory, we can add order.css to the value of runtime_deps:

dune
(runtime_deps order-item.css order.css)
(runtime_deps order-item.css order.css)

If you have many .css files, you can tell runtime_deps to copy all .css files over using Dune’s glob_files configuration:

dune
(runtime_deps (glob_files *.css))
(runtime_deps (glob_files *.css))

Check the Dune documentation for the different options for globs.

Add classes to JSX

Now we can add the appropriate classes to OrderItem.make’s JSX:

re
module OrderItem = {
  [@react.component]
  let make = (~item: Item.t) =>
    <tr className="item">
      <td className="emoji"> {item |> Item.toEmoji |> React.string} </td>
      <td className="price"> {item |> Item.toPrice |> Format.currency} </td>
    </tr>;
};
module OrderItem = {
  [@react.component]
  let make = (~item: Item.t) =>
    <tr className="item">
      <td className="emoji"> {item |> Item.toEmoji |> React.string} </td>
      <td className="price"> {item |> Item.toPrice |> Format.currency} </td>
    </tr>;
};

As well as Order.make’s JSX:

re
[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table className="order">
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr className="total">
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};
[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table className="order">
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr className="total">
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};

Finally, add a mel.raw extension node at the top of Order.re:

reason
[%%mel.raw {|import "./order.css"|}];
[%%mel.raw {|import "./order.css"|}];

Problems with mel.raw

This solution works well for our current build configuration, but falls apart if we change the module_systems field of the melange.emit stanza from es6 to commonjs. This results in a subtle runtime error caused by CommonJS needing require instead of import to import modules.

The mel.raw extension node is unsafe, but it is still useful for prototyping. Fortunately, Melange provides a more reliable way to import frontend assets.

Import using external

At the top of Order.re, replace our first mel.raw extension node with an external declaration:

reason
[%%mel.raw {|import "./order-item.css"|}]; 
[@mel.module "./order-item.css"] external _css: unit = "default"; 
[%%mel.raw {|import "./order-item.css"|}]; 
[@mel.module "./order-item.css"] external _css: unit = "default"; 

This essentially tells OCaml to assign the default export of the order-item.css module to the variable _css. The generated JavaScript looks like this:

javascript
import OrderItemCss from "./order-item.css";
var _css;
import OrderItemCss from "./order-item.css";
var _css;

Let’s break down the individual parts of the external declaration:

reason
[@mel.module "./order-item.css"] external _css: unit = "default";
[@mel.module "./order-item.css"] external _css: unit = "default";
  • mel.module is an attribute that tells the external declaration which module to import from
  • The external keyword tells OCaml this is a declaration for a value defined outside of OCaml, i.e. it comes from JavaScript[3]
  • _css: unit means the object we get back from the import is named _css and has type unit. We put an underscore in front of the name because we don’t intend to use this variable. Likewise, we give it a type of unit because it doesn’t have a meaningful value.
  • The "default" at the end tells OCaml to import the default export of the module.

TIP

A quick way to check what an external declaration compiles to is to use the Melange Playground. For example, here’s a link to the external declaration we just added.

Use CSS modules

Right now, the classes defined in the CSS files we’re importing are in the global scope. For non-trivial projects, it’s better to use CSS modules, which give us access to locally-scoped classes[4].

First, rename order-item.css to order-item.module.css, which turns it into a CSS module. Then change the corresponding external declaration:

reason
[@mel.module "./order-item.css"] external _css: unit = "default"; 
[@mel.module "./order-item.module.css"] external css: Js.t({..}) = "default"; 
[@mel.module "./order-item.css"] external _css: unit = "default"; 
[@mel.module "./order-item.module.css"] external css: Js.t({..}) = "default"; 

There are three changes of note:

  • We change the payload of the mel.module attribute to ./order-item.module.css to reflect the new name of the file
  • We rename the _css variable to css, since we intend to use the variable later
  • We change the type of css from unit to Js.t({..})[5]

If you look at your compiled app in the browser right now, you’ll see that this change breaks the styles, because the classes defined in order-item.module.css can no longer be accessed by the names we originally gave them. To access the locally-scoped classes, we must refactor the OrderItem component so that it accesses the class names through the css variable:

re
module OrderItem = {
  [@mel.module "./order-item.module.css"]
  external css: Js.t({..}) = "default";

  [@react.component]
  let make = (~item: Item.t) =>
    <tr className=css##item>
      <td className=css##emoji> {item |> Item.toEmoji |> React.string} </td>
      <td className=css##price>
        {item |> Item.toPrice |> Format.currency}
      </td>
    </tr>;
};
module OrderItem = {
  [@mel.module "./order-item.module.css"]
  external css: Js.t({..}) = "default";

  [@react.component]
  let make = (~item: Item.t) =>
    <tr className=css##item>
      <td className=css##emoji> {item |> Item.toEmoji |> React.string} </td>
      <td className=css##price>
        {item |> Item.toPrice |> Format.currency}
      </td>
    </tr>;
};

Recall that ## is the access operator for Js.t objects, so className=css##item is equivalent to className={css.item} in JavaScript. Note that we also moved the external declaration for ./order-item.module.css inside the OrderComponent module, since that’s the only place it’s used.

We have not seen the last of external declarations, as they are the primary way in which OCaml interacts with code written in JavaScript. See the Melange docs for more details.

Class names must be the same

The class names you use in .css files must be the same as the ones you use in your .re files. Try changing css##emoji in the OrderItem component to css##emojis.

What happens is that the styling for emojis silently breaks. This is the weakness of the CSS module approach, which requires that you manually keep all class names in sync. In a future chapter, we’ll introduce a type-safe approach to styling that doesn’t have this problem.


Excelsior! Madame Jellobutter likes how the order confirmation widget looks so far. But she plans to add more options for her current menu items, for example she’d like to have more than one type of sandwich. We’ll tackle that in the next chapter.

Overview

  • The mel.raw extension node embeds raw JavaScript inside OCaml code
    • It isn’t type-safe and you can usually use external instead
  • The runtime_deps field of melange.emit copies assets like .css files to the build directory
    • The glob_files term can be used to copy all files of a certain type
  • external declarations are used to import CSS or JS files
    • The mel.module attribute is used to specify which module or file to import

Exercises

1. Extension nodes like mel.raw can also be prefixed with % instead of %%. What happens if you replace %%mel.raw with %mel.raw?

reason
[%%mel.raw {|import "./order-item.css"|}]; 
[%mel.raw {|import "./order-item.css"|}]; 
[%%mel.raw {|import "./order-item.css"|}]; 
[%mel.raw {|import "./order-item.css"|}]; 
Solution

Changing %%mel.raw to %mel.raw will cause a compilation error in Vite because the generated JS code changes to

javascript
((import "./order.css"));
((import "./order.css"));

which isn’t valid JavaScript syntax. Changing it back to %%mel.raw will produce syntactically valid JS:

javascript
import "./order.css"
;
import "./order.css"
;

The general rule is that you should use %%mel.raw for statements, and %mel.raw for expressions.

2. Refactor the Order component so that it also uses an external declaration instead of mel.raw.

Solution

Order component using external declaration:

re
[@mel.module "./order.module.css"] external css: Js.t({..}) = "default";

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table className=css##order>
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr className=css##total>
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};
[@mel.module "./order.module.css"] external css: Js.t({..}) = "default";

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table className=css##order>
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr className=css##total>
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};

3. Replace your usage of mel.module with bs.module. What happens?

Solution

If you replace mel.module with bs.module, your code will fail to compile with this error message:

File "src/styling-with-css/Order.re", line 4, characters 4-13:
Error: `[@bs.*]' and non-namespaced attributes have been removed in favor of `[@mel.*]' attributes.
File "src/styling-with-css/Order.re", line 4, characters 4-13:
Error: `[@bs.*]' and non-namespaced attributes have been removed in favor of `[@mel.*]' attributes.

Basically, bs.module was the old name for the attribute, but it has been replaced by mel.module. This is worth mentioning because there’s still a decent amount of code out in the wild that uses bs.module.


View source code and demo for this chapter.



  1. Recall that in the Index modules you’ve written so far, you’ve never had to import any of the components you used that were defined in other files. ↩︎

  2. Quoted string literals are similar to multiline string literals in Python. Inside a quoted string literal, you don’t need to escape double quote or newline characters. ↩︎

  3. In native OCaml, external refers to functions and variables that come from C. ↩︎

  4. The “local scoping” of CSS modules isn’t quite like scoping in a programming language. Instead, class names defined in a .module.css file are obfuscated so that only OCaml/JS modules that import them directly can use them. ↩︎

  5. Js.t({..}) is the type signature for a Js.t object, which we first encountered in the Celsius Converter chapter. ↩︎