DEV Community

agj
agj

Posted on • Originally published at blog.agj.cl

Styling a static website in OCaml

Last time I posted about rewriting the code for my “Piclog” images blog in OCaml. After that I continued working on the website. The most apparent improvement is that I upgraded the design to better match current standards! But other than that, I also did some fun technical adjustments to how it's built.

The design still looks approximately the same, but it has a better-thought-out color palette and typography, has a dark mode, and is responsive.

Piclog screenshot

The menu now switches from an inline one to a dropdown on narrow viewports. I managed it using native declarative HTML popovers and some light media-querying (here's the relevant code), which means I don't have to keep any state, and in fact the entire site still has no JavaScript to speak of. I haven't really needed it so far, but if I do at some point, I have a few ideas of what to do, including using Alpine.js, or rolling my own solution using Js_of_ocaml.

I'm making some use of icons, which I copy as SVG files on build time from the Phosphor icons source. I would have used the font version (i.e. loading a font which contains all the icons,) but I wanted to keep the site lean, and for so few icons, the font is a bit too heavy.

While loading the icons as individual SVGs in <img> elements is a lightweight and simple solution, it does make it hard to adjust the color via CSS. So instead of that, I use the image as a mask via the mask-image CSS property on a plain <span>, and configure its background-color. The SVG-as-mask then “cuts out” the square <span> into the shape of the icon. I got this solution from a Stack Overflow answer.

Generating the stylesheet

I completely refactored how I apply styles. I started using the traditional approach of having an external CSS file which defines rules through classes, and applying such classes to the HTML elements. Now, though, most of the styling is done inline. The benefit is that markup and styles are co-located, which makes it easier to reason about which styles affect what. But if the styles were directly output as inline styles on each element (i.e. <div style="…">,) the resulting markup would get bloated. So what I've done instead is take inspiration from the rtfeldman/elm-css Elm package.

What that means is that during build time I collect styles for each element, hash them, and use that hash as a class name in order to generate the stylesheet. Here's the most relevant part of that code. Since the style rules are hashed, repeated rules are only placed once in the stylesheet, and the resulting CSS is of a very respectably small size. I don't even bother minimizing it—in fact I format it for readability. It's a single file generated for the whole website, so it gets loaded a single time.

It's a relatively simple yet lean solution! However, for styles that I know are unique to a single element, I use a special attribute that does insert styles inline and not in the generated CSS file, so it does require that tiny bit of manual optimization at times.

Here's a basic demonstration taken from the source: a function that generates the link to a category page.

let view_category ~(name : string) ~(url : string) ~(current : bool) : Element.t =
  El.a
    ~a:
      [ At.href url
      ; (if current then At.styles [ Css.bold ] else At.none)
      ; At.styles [ Css.view_transition_name name ]
      ]
    [ El.text name ]
;;
Enter fullscreen mode Exit fullscreen mode

Which generates this kind of markup:

<a class="_7fffffffd10d5435" href="/category/graphics">graphics</a>
Enter fullscreen mode Exit fullscreen mode

And this bit of CSS:

._7fffffffd10d5435 {
  font-weight: bold;
  view-transition-name: graphics;
}
Enter fullscreen mode Exit fullscreen mode

I take advantage of rule nesting to solve the need for more complex selectors, such as modifiers like :hover, @media queries, ::before and ::after pseudo-elements, @supports queries, and even selection based on parent or sibling nodes. See this artificial example, just for the sake of demonstration:

El.div
  ~a:
    [ (* Styles applied always. *)
      At.styles [ Css.bg_color_base_1 ]
    ; (* Only for dark theme. *)
      At.styles ~conditions:[ Css.Cond.dark ] [ Css.bg_color_base_9 ]
    ; (* A more complex selector: when the element has a `.some-sibling` sibling
         element. *)
      At.styles
        ~conditions:[ Css.Cond.custom ":has(.some-sibling) > &" ]
        [ Css.bg_color_base_5 ]
    ]
Enter fullscreen mode Exit fullscreen mode

Which is rendered as the following HTML and CSS rules:

<div class="_7fffffff8f095f56">Hi!</div>
Enter fullscreen mode Exit fullscreen mode
._7fffffff8f095f56 {
  background-color: var(--color-base-1);
  @media (prefers-color-scheme: dark) {
    background-color: var(--color-base-9);
  }
  :has(.some-sibling) > & {
    background-color: var(--color-base-5);
  }
}
Enter fullscreen mode Exit fullscreen mode

By the way, the Css module which I use above doesn't have much magic at all. The CSS definitions such as Css.bold are just a convenient way of defining a pair of strings (property + value,) and the conditions such as Css.Cond.dark also just hide the single string which is put in front of the {} brackets that define the nested rule. These rules can even be multi-nested, so it's possible to, for instance, set the color of an element in dark mode during hover.


There's other, more subtle improvements I made to the page and the code, such as calculating image dimensions and dominant color to improve how they look before they load, but I'll leave it at that for this post!

Top comments (0)