miso icon indicating copy to clipboard operation
miso copied to clipboard

The best way to handle style sheets in Miso application or component library

Open functora opened this issue 1 year ago • 15 comments

I would like to discuss the best way to handle style sheets in Miso application or component library. I think styles abstractions (whatever it is) should be:

  • Composable with library/application Haskell code.
  • Composable with each other.
  • More or less typed (optional).

At the moment I see couple of options, but they all are kinda bad:

  1. Using the standard external CSS file. The problems are:
  • I don't see the right way to export it as a part of the library (these are no css-sources in cabal, unlike js-sources)
  • CSS class names are just Text which should be the same across Haskell (class_) and CSS codebase. Hard to maintain and refactor. Prone to errors.
  1. Using inline <style> tags next to every small component. I think kinda bad idea, and I'm not sure how to scope styles locally to component this way. Not sure it is possible or easy (not an expert in CSS).
  2. Using style_ attribute functions and different variants of compositions of it. It's a bit better, but:
  • Can not define styles for pseudo-elements (::hover, ::before, ::after etc)
  • Can not define advanced CSS animations like @keyframes my-animation-name
  1. Doing some smart CSS code generation from some Haskell style sheet source (.hs files), exporting class names (functions, utilities) used in Miso lib/app codebase, but having additional Setup step for the app/lib which doing corresponding CSS file code generation somehow.
  • Kinda sounds hard to do (maybe not, idk).

If I did miss some documentation or cool advanced CSS examples in Miso, please let me know. Best regards!

functora avatar Oct 04 '24 01:10 functora

This is an important topic. There's a lot of different schools of thought on how to create and manage CSS in applications (CSS-in-JS, using preprocessors (sass, etc.), SMACSS, BEM, utility classes / tailwind). In an attempt to please everybody we did not make any hard decisions.

With that said, for preprocessors there is a project called clay that could be integrated with miso, providing additional type safety. Also I believe there are attempts to wrap tailwind classes with miso's class attribute (otherwise wouldn't be difficult to do this on your own).

dmjio avatar Jan 20 '25 19:01 dmjio

@functora http://fvisser.nl/clay/ from @sebastiaanvisser

dmjio avatar Jan 20 '25 19:01 dmjio

Also I believe there are attempts to wrap tailwind classes with miso's class attribute (otherwise wouldn't be difficult to do this on your own).

Hi @dmjio, thanks so much for this project. Do you have any references for those Tailwind+Miso integration attempts?

dhess avatar Jan 20 '25 20:01 dhess

@functora,

I have come up with a partial solution for CSS in this PR https://github.com/dmjio/miso/pull/859

It adds styles :: [CSS] to App, and it will guarantee that your styles are present in Document before the first draw occurs (so no rendering w/o styles will occur). It's not a full CSS-in-JS approach, but it does push us in that direction.

We also now have Component which encapsulates an App, and this opens up the door to use styles in sub Component in a way that resembles the CSS-in-JS technique. So I'm still pondering that, but will not force a solution until it arises naturally (like Component). Am studying other techniques and libraries to see which is best for us.

This PR (#859) does solve an important problem which is the ops / dev effort around updating an external stylesheet, that is now ameliorated. So Todo-MVC requires no fiddling with index.html post-build. cc @georgefst @brakubraku

For defining inline styles (and maybe even for CSS-in-JS), we might want something like Clay, or @intolerable's stitch library. Kind of leaning towards stitch because it's more stringly-typed and therefore harder for the LLMs to get it wrong (they still do sadly, but not as bad as they do for clay since the API has changed a bit the LLMs generate code that doesn't compile).

@dhess

I was able to get some of the LLMs to generate miso's View code with Tailwind classes. It doesn't fully compile, but getting the class names on there is 90% of the work. The miso-from-html package actually worked better for this imo.

For now my recommendation is to convert the Tailwind Component library HTML/CSS code using this tool:

  • https://github.com/dmjio/miso-from-html

Here are some attempts by DeepSeek to generate miso code for Tailwind CSS

  • https://gist.github.com/dmjio/0851afa4898253cfa9d67fe323bd0f3a
  • https://gist.github.com/dmjio/a029c45b73ea00cc40c6ff075df223f7

dmjio avatar Apr 08 '25 05:04 dmjio

If we do decide to expose a stitch like interface into miso (Miso.Style), then we could also create a miso-from-css tool as well to generate the DSL code (just like we do with lists for View action in miso-from-html). Since the LLMs do get confused at times when it comes to Haskell DSL syntax

Something /like/ this could be generated, if the user wanted something inline, otherwise we could bias towards class-based approaches like tailwind in API and documentation, but will always allow users to do inline styles.

view :: Model -> View Action
view model = a_
  [ onClick GoHome
  , id_ "button"
  , style_ buttonStyle
  ]  
  [ "Click Me" 
  ]

buttonStyle :: Css
buttonStyle = do
  "color" .= "#448"
  "&:hover" ?
    "color" .= "#44F"
  "font" -: do
    "family" .= "Open Sans, sans"
    "size" .= "1.4em"
    "weight" .= "bold"

dmjio avatar Apr 08 '25 05:04 dmjio

@functora let me address each point you made above

At the moment I see couple of options, but they all are kinda bad: Using the standard external CSS file. The problems are:

I don't see the right way to export it as a part of the library (these are no css-sources in cabal, unlike js-sources)

CSS class names are just Text which should be the same across Haskell (class_) and CSS codebase. Hard to maintain and refactor. Prone to errors.

This should now be solved in #859, the browser will also cache this too (for cache busting append hash, etc.).

So I'd recommend using a newer GHC (like the @terrorjack's WASM backend) since it supports TemplateHaskell, the CSS can be read into the WASM payload at compile time and be appended to <head> before the page renders (this avoids any need for css-sources).

Using inline <style> tags next to every small component. I think kinda bad idea, and I'm not sure how to scope styles locally to component this way. Not sure it is possible or easy (not an expert in CSS).

I agree we do need to come up with some way to style and export Component for reuse. Like described in the CSS-in-JS wikipedia

import styled from 'styled-components';
// Create a component that renders a <p> element with blue text
const BlueText = styled.p`
  color: blue;
`;

<BlueText>My blue text</BlueText>

We can create our own BlueText Component as well. Since Component do hold styles :: [CSS]. So miso could, behind the scenes append a <style> for the user. We might need to extend CSS to be specific that these styles are applied locally to a Component. So I think an extension

data CSS
  = Style MisoString
  | Href MisoString
  | ComponentStyle MisoString -- new constructor

Could be something that we experiment with. This will let styles be local to a Component, we might consider also removing them during the unmount phase as well. But I think we want to look to React for some inspiration as well. But they seem pretty hands-off with respect to styling as well.

Per their docs,

Note that this functionality is not a part of React, but provided by third-party libraries. React does not have an opinion about how styles are defined; if in doubt, a good starting point is to define your styles in a separate *.css file as usual and refer to them using className.

But it does seem industry is gravitating towards component-local styling, and again we should be able to support now with #859

Using style_ attribute functions and different variants of compositions of it. It's a bit better, but: Can not define styles for pseudo-elements (::hover, ::before, ::after etc) Can not define advanced CSS animations like @keyframes my-animation-name

That is definitely a problem, and one that can be solved with a DSL and Component-local styling.

Doing some smart CSS code generation from some Haskell style sheet source (.hs files), exporting class names (functions, utilities) used in Miso lib/app codebase, but having additional Setup step for the app/lib which doing corresponding CSS file code generation somehow.

I'm a little reticent to generate CSS on behalf of the user (which I think you're alluding to here - correct me if I'm wrong), solely because it's easy to get it wrong since the state space of what they could want is large.

But I do agree we need a way to encapsulate styling logic to specific Component. I'm not convinced attempting to provide strong types for it is also a benefit (yet). It's also difficult to do w/o introducing GADTs given our formulation (which we will never do 🤞🏼 to keep things simple).

Kinda sounds hard to do (maybe not, idk) If I did miss some documentation or cool advanced CSS examples in Miso, please let me know. Best regards!

@functora feel free to join the Matrix chat to discuss Component local styling, there's a link to it on https://haskell-miso.org.

dmjio avatar Apr 08 '25 06:04 dmjio

I like the aesthetic of this approach as well https://styled-components.com/ , and this is considered CSS-in-JS

dmjio avatar Apr 08 '25 06:04 dmjio

I think it might be a good idea to make a CSS lexer, parser, QQ. This way we can verify statically semantically correct CSS, and also provide interpolation

dmjio avatar May 27 '25 19:05 dmjio

https://github.com/dmjio/miso/pull/984

dmjio avatar Jun 15 '25 21:06 dmjio

@functora #984 should address 3) in your original post

Example use here

dmjio avatar Jun 17 '25 00:06 dmjio

Also @functora , @dhess , regarding components and tailwind, this is a nice style (Shad minus React), uses tailwind. We will use this for the new Components example

https://basecoatui.com/

Also, a miso-tailwind integration is probably not going to happen formally because

  1. There's many thousands of tailwind classes, so we can't express all of these as defined functions
  2. It's just classes so it should be fine for users to just use class_ or classList_

So, tailwind is supported technically already, but just use class_ or classList_, I might make classes :: [MisoString] -> Attribute action as well

dmjio avatar Jul 14 '25 23:07 dmjio

https://github.com/dmjio/miso/commit/c5ed9cb3bbbd152ebe0f47739337bddb3a1cb681

dmjio avatar Jul 16 '25 04:07 dmjio

@dhess @functora related to basecoat link above

https://tweakcn.com/

dmjio avatar Jul 18 '25 13:07 dmjio

https://github.com/dmjio/miso/issues/1113

dmjio avatar Aug 18 '25 11:08 dmjio

https://ui.haskell-miso.org

dmjio avatar Nov 27 '25 01:11 dmjio