miso-css: wrapper over miso checking CSS classes applicability through dependent types

[ bsd3, css, html, library, miso, template-haskell ] [ Propose Tags ] [ Report a vulnerability ]

Motivation

miso-css is an evolutionary step ahead from css-class-bindings.

CSS class of an atomic selector can be applied to any DOM element, but that is not true for classes used in composite selectors. Rules with partially matched selectors are silently ignored by browser and this open a door for introducing bugs during consequent changes. Css-class-binding just cannot cope with such problem and miso-css uses dependent types to track what CSS classes can applied to HTML elements.

Usage

miso-css runs a css parser to extract CSS selectors and generates Haskell constants for every CSS class with correspondent name that is found in the input. A type of such constant describes all possible ways the class can used in DOM.

Besides that the library ships E type representing an HTML element and a set of operators for constructing tags and combining them in DOM tree. E is miso VNode type protected with a few type parameters.

Composing tags

Before jumping straight to style application lets get familiar with syntax for tag composition because it is different in vanilla miso.

Appending a child

div_ </ p_
<div>
  <p></p>
</div>
Adding a sibling

ul_ </ li_ </ li_
<ul>
  <li></li>
  <li></li>
</ul>
Appending a child to child

body_ </ (section_ </ p_)
<body>
  <section>
    <p></p>
  </section>
</body>
Adding CDATA

a_ <@ "click"
<a>click</a>
Adding a raw miso DOM chunk

import Miso.Html qualified as MH
import Miso.Html.Property qualified as MH

go = div_ =< MH.p_ [] [ "h" ]
<div>
  <p>h</p>
</div>
Adding tag attribute

a_ =<| atr @"href" "http://link.com"
<a href="http://link.com"></a>
Binding event handler

button_ =! onClick YourActionDc
Applying CSS class

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|.red { color: red; }|]

div_ =. red
<div class="red"></div>
Adding tag ID

Handmade tag id:

div_ =# ElementId "footer"

Generated tag id:

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|#footer { color: red; }|]

div_ =# Footer
<div id="footer"></div>
Mix all at once

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|.form .red { color: red; }|]

div_ =. form =# ElementId "footer"
  </ (a_ =. red =<| atr @"href" "/click.php?x=1"
      </ (span_ <@ "Click me"))
<div class="form" id="footer>
  <a class="red" href="/click.php?x=1">
    <span>Click me</span>
  </a>
</div>

Breaking rules

Until now all above samples must be valid and should type check. This section enumerates HTML snippets with ill-applied classes, expected errors and comments.

There can be only one

An element ID can be used once in a HTML document.

div_ =# ElementId "Duncan MacLeod"
  </ div_ =# ElementId "Duncan MacLeod"
Couldn't match type: '[DuplicatedId "Duncan MacLeod"]
               with: '[]
Parent class is missing

[css|.a .b {}|]

div_ =. b

The error message is a list of triples where first element is a list of not applied classes, ids (hashes), tag names or attribute names.

[([C "a"], [], [])]

Class a and b are missing:

[css|.a .b .c {}|]

div_ =. c
[ ([C "b"], [], [])
, ([C "a"], [], [])
]
B element

When selector with a child relation is partially applied the triple contains B element. It is a synthetic element preventing the failed rule from matching later somewhere upper in DOM by an accident.

[css|.a > .b {}|]

div_ </ div_ =. b
[([B, C "a"], [], [])]
One of classes is missing

Second element of triple is a list of applied classes. It helps to understand what worked out and what didn’t in a composite selector.

[css|.a.b > .c {}|]

div_ =. a </ div_ =. c
[([B, C "b"], [C "a"], [])]
Sibling is missing

The third element of triple explains sibling errors.

[css|.a + .b {}|]

div_ </ div_ =. b

Class a is not applied:

[([B], [], [[ [B], [C "a"]]])]

Non-leaf classes constraints

By default non-leaf classes in selector don’t contribute to constraints. e.g.

.a > .b .c

b doesn’t require a as immediate parent it is handled by constraints form c.

It is possible to generate constraints for b to make checking even stricter, though in such mode following DOM can’t pass type check:

enableRulesForNonLeafClasses

[css|.a > .b .c {}|]
test_t = testGroup cssAsLiteralText
  [ doNotTcNoBr [] [[[(JustNow, [B], [C "a"], [])]]] $
      div_ =. a </ (div_ =. b </ (div_ =. b </ div_ =. c))
  ]

E type

data E
     model
     action
     (en :: Symbol)
     (es :: ElementStructure)
     (re :: Maybe Root)
     (ei :: Maybe Symbol)
     (atrs :: [Symbol])
     (knownIds :: KnownIDS)
     (cls :: [Symbol])
     (l :: [[[Seg]]])
     (children :: [[SubSeg]])

First two parameters model and action are forwarded to miso VNode type.

en - tag name

In ghci session:

:t div_
div_
  :: E model
       action
       "div"
...
es - element structure

Most often its value is Composite which means that the element could have children. Es parameter of CDATA element is Atomic.

re - root indicator

It is a root tag indicator. A root tag cannot be adopted.

[css|:root > .a {}|]
ei - HTML tag hash

div_ =# ElementId "Duncan"
atrs - names of tag attributes

:t a_ =# ElementId "x" =<| atr @"href" "/click.php?x=1"
...
       ["href", "id"]
...
knownIds - hashes used in tag descendants

:t div_ =# ElementId "x" </ div_ =# ElementId "y" </ div_ =# ElementId "z"
...
       (KnownIds '[] ["x", "y", "z"])
...
cls - classes applied to tag

Classes applied to children and descendants are not included.

:t div_ =. a =. b </ div_ =. c
...
       ["b", "a"]
...
l - ancestor constraints

The parameter describes requirements to be satisfied in ancestors of the tag.

[css|.a .b {}|]
:t div_ =. b
...
       '[ '[['(AutoClean, '[], '[], '[]),
             '(NowOrLater, '[C "a"], '[], '[])]]]
...
children

List of lists of children subselectors in reverse order.

[css|.a {} .b {}|]
:t div_ </ ul_ =. a =. b </ ol_ =# ElementId "x"
...
       [[I "x", T "ol"], [T "ul", C "b", C "a"]]
...

Hello World

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
module Miso.Css.Test.HelloWorld where

import Miso ( component, App, CSS(Style), Component(styles), View )
import Miso.Css
import Prelude

type Model = ()
type Action = ()

-- default name is "cssAsLiteralText"
renameCssTextConst "cssFromQq"

[css|
.c .b .a {
  color: #fc2c2c;
}
|]

-- instead of quasi-quoted CSS
-- the whole CSS file can be included with:
--   includeCss "assets/style.css"

app :: App Model Action
app = (component () pure viewModel)
  { styles = [ Style cssFromQq ] }

{-
viewModel produce following HTML snippet:

    <div class="c">
      <div class="b">
        <button class="a">
          Submit
        </button>
      </div>
    </div>

html_ and body_ don't produce tags,
because miso mount cannot be higher than body tag.

they serve just for type checking purpose
(e.g. html_ satisfies :root pseudo class)

-}
viewModel :: Model -> View Model Action
viewModel () = toView . html_ . body_ $
  div_ =. c
  </ (div_ =. b
       </ (button_ =. a
            <@ "Submit"))

Development environment

HLS should be available inside the default dev shell.

$ nix develop
$ emacs src/*/*/Qq.hs &
$ cabal build
$ cabal test --test-option=--hide-successes

miso-css was developed with miso v1.9

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.0.1, 0.0.2
Change log changelog.md
Dependencies add-dependent-file (>=0.0.2 && <1), base (>=4.7 && <5), containers (<1), css-parser (<1), filepath (<2), miso (>=1.11 && <1.12), mtl (<3), tagged (<1), template-haskell (<3), text (<3) [details]
Tested with ghc ==9.12.2
License BSD-3-Clause
Copyright Daniil Iaitkov 2026
Author Daniil Iaitskov
Maintainer dyaitskov@gmail.com
Uploaded by DaniilIaitskov at 2026-06-02T17:01:55Z
Category Miso, HTML, CSS, Template Haskell
Home page http://github.com/yaitskov/miso-css
Bug tracker https://github.com/yaitskov/miso-css/issues
Source repo head: git clone https://github.com/yaitskov/miso-css.git
Distributions
Downloads 2 total (2 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]