Default stylesheet

This page explains how the default stylesheet used by all pages on this site is defined. It is Haskell code using Clay to generate the CSS stylesheet.

Preamble

As with all Haskell applications, it starts out with defining the main module and the modules it imports.

module Main (main) where

import Clay
import Clay.Media qualified as Media
import Web.Site.Styles (narrowWidth, wideWidth)

A few function names from the Prelude module conflict with HTML element names or CSS keywords. It is more convenient to use the HTML or CSS names directly, so we hide the conflicting names from the Prelude.

import Prelude hiding (div, filter, not, (**))

And of course, we should output our generated CSS stylesheet.

main :: IO ()
main = putCss defaultStyle

Everything

Next, we define the high-level function which calls all the other functons generating the stylesheet.

defaultStyle :: Css
defaultStyle = do

We want the generic styles to be generated first, because they should be overridden by later styles in specific contexts.

  genericStyle

We then generate the styles which are only relevant to specific contexts.

  codeStyle
  articleStyle
  tableOfContents
  figures

Finally, the media-specific styles should be generated last. They may need to override previously generated styles so that they are more appropriate to specific media such as narrower screens.

  mediaStyles

Generic styles

These are styles which apply generally. In other words, if there are no styles which need to apply more specifically in a certain context, these are the styles which are applied.

genericStyle :: Css
genericStyle = do

I basically chose the default fonts based on my arbitrary preferences. The only condition I had was that they are expected to be available in any browser. Legibility also takes precedence over anything else such as looking prettier. I can’t stand jagged edges in text, so I specify that text be justified.

  html ? do
    fontFamily ["Georgia", "Garamond"] [serif, sansSerif, monospace]
    textRendering optimizeLegibility
    textAlign justify
    lineHeight $ unitless 1.25

In narrow screens, it often feels like there is a great waste of empty space without words being hyphenated as appropriate, so automatic hyphenation is enabled. I think hyphenation is fine in general, so I don’t restrict its use to only narrow screens.

    hyphens auto

The rest of the generic styles were basically tweaked by me until they achieved a minimal level of acceptability. Unexpectedly, I ended up liking the minimal look, so I will probably stick with it.

  body ? do
    marginTop $ em 2
    marginLeft $ pct 10
    marginRight $ pct 10
    marginBottom $ em 2

  headings

  footer ? do
    fontFamily ["Courier New"] [monospace, sansSerif]
    fontSize $ em 0.75
    marginTop $ em 1

  footer |> nav ? do
    paddingTop $ em 1.5
    a ? paddingRight (em 1)

  dt ? do
    fontWeight bold
    marginBottom $ em 0.25

  dd ? do
    marginBottom $ em 1

One thing to note is that I want some spacing between list items. So there is a margin between each list item. There is only an extra margin for a list if it is a direct child of a list item, since there would be extra margins from other block elements if the list was not a direct child.

  li |+ li ? marginTop (em 0.75)

  li |> (ul <> ol) ? marginTop (em 0.75)

Heading styles

For the sole reason that I use monospace fonts with code, I have the headings in a monospace font as well. The headings are not justified.

Except for the first-level heading which basically serves as the displayed title for the page, I add dotted underlines to distinguish the headings from normal text. Different levels of headings are distinguished by different sizes; the different sizes may be a bit too subtle, but I’m not sure what to do about it. At least the heading levels will be obvious in the table of contents.

Later, heading levels will also be distinguished by fading colors, when the colors are specified for each preferred color scheme for specific media.

headings :: Css
headings = do
  h1 <> h2 <> h3 <> h4 <> h5 <> h6 ? do
    fontFamily ["Courier New"] [monospace, sansSerif]
    textAlign $ alignSide sideLeft

  h1 ? do
    fontSize $ em 2
    fontStyle italic

  h2 <> h3 <> h4 <> h5 <> h6 ? do
    textDecorationLine underline
    textDecorationStyle dotted

  h2 ? fontSize (em 1.8)
  h3 ? fontSize (em 1.5)
  h4 ? fontSize (em 1.25)
  h5 ? fontSize (em 1.1)

Content-specific styles

Code

Style for code snippets.

codeStyle :: Css
codeStyle = do
  div # ".sourceCode" ? do
    borderStyle solid
    borderWidth $ px 1
    marginRight $ em 1
    marginLeft $ em 1
    sym padding $ em 0.5

Articles

Style for article elements.

articleStyle :: Css
articleStyle = do
  article |> section # ".byline" ? do
    fontFamily ["Verdana"] [sansSerif, serif, monospace]
    fontSize $ em 0.7
    p ? do
      marginTop $ em 0.2
      marginBottom $ em 0.2

Table of contents

Style for table of contents.

tableOfContents :: Css
tableOfContents = do
  nav # ".toc" ? do
    marginTop $ em 1
    marginBottom $ em 1
    sym padding $ em 1
    borderStyle solid
    borderWidth $ px 1

    h2 ? do
      fontFamily ["Georgia", "Garamond"] [serif, sansSerif, monospace]
      fontSize $ em 1.2
      fontStyle normal
      fontWeight bold
      textDecorationLine none
      textDecorationStyle none
      marginTop $ em 0.1
      marginBottom $ em 0.25

    ul <> (ul ** (ul <> li)) ? do
      paddingLeft $ em 0.75
      marginTop $ em 0.1
      marginBottom $ em 0.1
      listStyleType none

Images

Style for images. One thing to note is that we want images to fit well with everything else. The maximum width is limited to the surrounding content width. The maximum height is limited to 60% of the display area so that some content before and after an image can also be visible, which in some cases would make it easier to keep track of what the image is supposed to explain.

figures :: Css
figures = do
  figure ? do
    display block
    paddingTop $ em 0.5
    paddingBottom $ em 0.5
    marginTop $ em 1
    marginBottom $ em 1
    marginLeft auto
    marginRight auto
    textAlign center

    img ? do
      display block
      marginLeft auto
      marginRight auto
      maxWidth $ pct 95
      maxHeight $ vh 60

    figcaption ? do
      display block
      fontFamily ["Verdana"] [sansSerif, serif, monospace]
      fontSize $ em 0.9
      marginTop $ em 0.5
      marginLeft auto
      marginRight auto

Media-specific styles

If the width is too narrow, we reduce the margins and paddings. Conversely, if the width is too wide, we limit the width of a page so that we do not have horrendously wide walls of text. We also set the colors depending on whether the preferred color scheme is light mode or dark mode.

mediaStyles :: Css
mediaStyles = do
  query Media.all [Media.maxWidth narrowWidth] $ do
    body ? sym margin (em 1)

    ul <> ol ? do
      marginLeft $ em 0.5
      paddingLeft $ em 0.5

  query Media.all [Media.minWidth wideWidth] $ do
    body ? do
      width wideWidth
      marginRight auto
      marginLeft auto

  footerPosition

  query Media.all [Media.prefersColorScheme Media.light] lightColorScheme

  query Media.all [Media.prefersColorScheme Media.dark] darkColorScheme

We define this function to map each heading level to a color, which makes it convenient to define heading colors when setting the colors programmatically based on the heading level.

headingColors :: (Integer -> Color) -> Css
headingColors mapColor = do
  h1 ? fontColor (mapColor 1)
  h2 ? fontColor (mapColor 2)
  h3 ? fontColor (mapColor 3)
  h4 ? fontColor (mapColor 4)
  h5 ? fontColor (mapColor 5)
  h6 ? fontColor (mapColor 6)

If the screen height is large enough so that it would not occupy more than half of the screen, I would like to keep the footer stuck to the bottom. To prevent other content from overlapping with the footer, we will also later explicitly set the background color for the footer.

footerPosition :: Css
footerPosition = do
  query Media.all [Media.minHeight $ em 24] $ do
    footer ? do
      position sticky
      bottom nil
      paddingBottom $ em 0.5

Light mode

Color scheme to use in light mode.

lightColorScheme :: Css
lightColorScheme = do
  html ? do
    color black
    backgroundColor white

  footer ? do
    backgroundColor white
    borderTop (px 1) solid black

  a # link ? color blue
  a # visited ? color purple

  headingColors headingColor

  nav # ".toc" ? do
    borderColor lightgrey
    backgroundColor $ rgb 240 240 240

  div # ".sourceCode" ? borderColor lightgrey

  where
    headingColor n = rgb (n * 20) (n * 20) (100 + n * 10)

Dark mode

Color scheme to use in dark mode.

darkColorScheme :: Css
darkColorScheme = do
  html ? do
    color white
    backgroundColor black

  footer ? do
    backgroundColor black
    borderTop (px 1) solid white

  a # link ? color cyan
  a # visited ? color pink

  headingColors headingColor

  nav # ".toc" ? do
    borderColor dimgrey
    backgroundColor $ rgb 20 20 20

  figure |> img # (not (byClass "keep-colors") <> "src" $= ".svg") ? do
    filter (invert $ pct 100)

  div # ".sourceCode" ? borderColor dimgrey

  where
    headingColor n = rgb (255 - n * 20) (255 - n * 20) (155 - n * 10)

See also