miso-css: wrapper over miso checking CSS classes applicability through dependent types
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_ =. bThe 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_ =. bClass 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
Modules
[Index] [Quick Jump]
Downloads
- miso-css-0.0.1.tar.gz [browse] (Cabal source package)
- Package description (as included in the package)
Maintainer's Corner
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.9 && <2), 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-02T16:47:58Z |
| 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] |