A resolution switching responsive images template with Haskell and IHP

, Haskell


Providing different file sizes for images on a web page reduces loading time on small devices. Here’s a simple functional programming way to use the srcset and sizes HTML attributes (see MDN Web Docs) with a PHP style template.

Rendering HTML

To build HTML views, the Haskell web framework IHP provides a JSX-like syntax called HSX. HSX is type-checked and compiled to Haskell code at compile-time. Here is a function rendering an image inside an aspect ratio box (see below for an explanation and the CSS). It provides several additional source images along with hints to help the browser pick the smallest sufficient file size for the respective display.

renderImgFullWidth :: Text -> Html
renderImgFullWidth imgName = [hsx|
    <div style="--aspect-ratio:3/1;">
        <picture>
            <source media={imgMinWidthHD} srcset={imgPathStaticHD imgName}/>
            <source media={imgMinWidthBig} srcset={imgPathStaticBig imgName}/>
            <source media={imgMinWidthMedium} srcset={imgPathStaticMedium imgName}/>
            <img src={imgPathStaticSmall imgName} class="image-full-width" alt={imgName}/>
        </picture>
    </div>
|]

In IHP, this function can be placed in Application/Helper/View.hs, so that it is available everywhere. It can then be used in a view like this:

html WelcomeView = [hsx|
        {renderImgFullWidth "image-name"}
|]

Image parameters and paths

The render function, besides the image name (which can be hardcoded on static pages or read from the database) also needs the minimum width information and a file path, which can be set up like so:

data ImgParams = ImgParams
    { minWidth :: Integer
    , filesize :: Integer
    }

imgHD     :: ImgParams
imgHD      = ImgParams {minWidth = 1201, filesize = 1920}
imgBig    :: ImgParams
imgBig     = ImgParams {minWidth = 961, filesize = 1110}
imgMedium :: ImgParams
imgMedium  = ImgParams {minWidth = 768, filesize = 690}
imgSmall  :: ImgParams
imgSmall   = ImgParams {minWidth = 1, filesize = 545}

imgPathStaticHD     :: Text -> Text
imgPathStaticHD     imgName = "../img/" <> imgName <> "_" <> show (filesize imgHD)     <> ".webp"
imgPathStaticBig    :: Text -> Text
imgPathStaticBig    imgName = "../img/" <> imgName <> "_" <> show (filesize imgBig)    <> ".webp"
imgPathStaticMedium :: Text -> Text
imgPathStaticMedium imgName = "../img/" <> imgName <> "_" <> show (filesize imgMedium) <> ".webp"
imgPathStaticSmall  :: Text -> Text
imgPathStaticSmall  imgName = "../img/" <> imgName <> "_" <> show (filesize imgSmall)  <> ".webp"

Thus, the image files should in this example be placed inside the static folder in a subfolder called /img, have an image name which is needed to call the rendering function, and a file size separated by an underscore, and be .webp. The image parameters depend on the layout: minWidth is a breakpoint, the filesize could be any “code”, with image width in pixel being the most obvious choice.

CSS for the aspect ratio box

It’s a good thing to avoid cumulative layout shift, not only because of users experience unexpected layout shifts, but also because it impairs performance. There are numerous ways to avoid it; here, we have an adaption of CSS-Tricks > Aspect Ratio Boxes > Using Custom Propierties. It uses the numbers given in the div’s class to calculate the aspect ratios and thereby reserve the right amount of space for the element which is then loaded.

[style*="--aspect-ratio"] > :first-child > .image-full-width {
  width: 100vw;
}
[style*="--aspect-ratio"] > :first-child > .image-full-width {  
  height: auto;
} 
@supports (--custom:property) {
  [style*="--aspect-ratio"] {
    position: relative;
  }
  [style*="--aspect-ratio"]::before {
    content: "";
    display: block;
    padding-bottom: calc(100vw / (var(--aspect-ratio)));
  }  
  [style*="--aspect-ratio"] > :first-child > .image-full-width {
    position: absolute;
    top: 0;
    height: 100%;
  }
}

See it in action

The full widht images on hanken-entrup.de use the above code, and the featured images of the blog posts („Artikel“) and selling points („Verkaufsstellen“) use a container-width variant of it.

If you find a mistake or see room for improvement, please let me know! Thank you Lukas Penner!