dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Scoped CSS Modules

Open itsezc opened this issue 2 years ago • 8 comments

Specific Demand

Scoping CSS modules within a Dioxus component (file), allows for fine grain control over the components style.

Implement Suggestion

I'm inspired by SvelteKit's implementation: https://svelte.dev/tutorial/styling

Currently this is possible through a String, but it'd be great if a macro was introduced to allow for error handling, especially useful when down the line Blitz / other renderers becomes the primary target and certain styling features are unavailable.

itsezc avatar Jun 15 '23 02:06 itsezc

If we leverage manganis, a straight forward solution can be found here - we parse css with the asset! macro, contained class names become unique internally, then exposed by the asset object as fields. E.g.

css:

.card {}

rsx:

static CSS: CssAsset = asset!("/assets/component_name.css", AssetOptions::css().with_unique_classes());

let classes = &CSS.classes;
rsx! {
    div { class: classes["card"]  }
}

CSS.classes is a const map. So it will succeed or fail at compile time if a class exists or does not in the stylesheet. The css classes get added a unique hash. e.g.

.card-dkgsal {}

Note: A with_unique_ids may also be useful for some cases, exposed like CSS.ids["id_name"]

Happy to work on this if this gets accepted.

mcmah309 avatar Nov 25 '25 08:11 mcmah309

There is a css_module macro that generates random typesafe classes here which is currently hidden from the documentation because the CSS parsing is unreliable

ealmloff avatar Nov 25 '25 18:11 ealmloff

I see, I don't think writing our own css parser a good idea -https://github.com/DioxusLabs/dioxus/blob/1d2dec11011d581e1e756d4b879004415958c85c/packages/manganis/manganis-core/src/css_module.rs#L102 (probably why it is "unreliable"). Even Blitz uses an external css parser - cssparser. Generating a unique struct is also option (as in the macro). But it is less portable and solves a different case I imagine - having the class names exposed to the ide, rather just than at compile time. But even this is tricky, since the macro won't rebuild in the ide if the css file changes. Which would annoy programmers. One only gets the accurate view at build time. A const map is still probably best here and fits into our existing asset! syntax while being more portable.

mcmah309 avatar Nov 26 '25 06:11 mcmah309

I don't think writing our own css parser a good idea

Yes, the custom CSS parser is the reason it is unreliable and something that would need to be removed before this is restored. I think was mostly driven by the type safety requirements. cssparser is a very heavy weight dependency to include in the manganis proc macro. If we could move the css processing into the CLI, that would be ideal.

static CSS: CssAsset = asset!("/assets/component_name.css", AssetOptions::css().with_unique_classes());
let classes = &CSS.classes;

rsx! {
    div { class: classes["card"]  }
}

CSS.classes is a const map. So it will succeed or fail at compile time if a class exists or does not in the stylesheet.

With the asset! approach you can deserialize the struct in static code after the linker runs, but "card" is a runtime argument to Index so I don't think we can do a compile time check to make sure card exists with this syntax

ealmloff avatar Nov 29 '25 01:11 ealmloff

cssparser is a very heavy weight dependency to include in the manganis proc macro.

Yes, but proc macros only have to be built once then they are cached and never rebuilt unless a the version is updated. So it shouldn't effect the speed of using the macro for development

If we could move the css processing into the CLI, that would be ideal.

How would this look? I am not familiar with the internals of the cli. I know it basically is it's own build tool that wraps cargo at a few points.

With the asset! approach you can deserialize the struct in static code after the linker runs, but "card" is a runtime argument to Index so I don't think we can do a compile time check to make sure card exists with this syntax

Yes, const index is still unstable. So would probably have to look like classes.get("card").

An Alternative would be to have the asset! macro generate a macro_rules macro - class!. Which can be used like class!["card"]. Though collisions are possible for the macro name so, so we may want to wrap it in a mod or recommend the asset! macro for this case is wrapped in a mod if multiple are used in the same file.

mcmah309 avatar Nov 29 '25 03:11 mcmah309

How would this look? I am not familiar with the internals of the cli. I know it basically is it's own build tool that wraps cargo at a few points.

The manganis asset system works by bundling statics into a new link section that the CLI then reads and replaces at build time. With scoped css modules, that would let us parse the css module at build time in the CLI and then insert back information about what classes are available into the static

ealmloff avatar Dec 01 '25 15:12 ealmloff

Stylance supports hot-reloading for CSS modules. I’ve experimented with it — the real-time compilation works great. Is it possible to integrate the same capability into Dioxus?

Image Image Image

Implement Suggestion

Here is the experimental repository project - technical-backup/dioxus. Start it using cargo dev.

  1. First, watch for updates to the .module.css files and automatically generate the integrated CSS.
Image
  1. Then, import the integrated CSS globally. Because the CSS file may change or delete content, using asset would cause multiple versions of the same class to take effect. Using include_str ensures that hot reloading does not produce side effects.
Image
  1. Use import_crate_style to import the .module.css files.
Image

zdu-strong avatar Dec 04 '25 22:12 zdu-strong

Oh perfect. The way I see it, we could definitely replace our bespoke parser with their core. I like that they use mod too with static strs rather than a generated a struct. Though I don't think we could go this route, since asset loading is lazy by default and looking at the generated code, we currently notify that the asset is being used by a deref trick on first use which is pretty smart.

fn deref(&self) -> &Self::Target {
    static CELL: OnceLock<()> = OnceLock::new();
    CELL.get_or_init(move || {
        let doc = document();
        doc.create_link(
            LinkProps::builder()
                .rel(Some("stylesheet".to_string()))
                .href(Some(ASSET.to_string())) // Will notify the asset is needed here I believe
                .build(),
        );
    });

    self.inner
}

I'll work on something.

mcmah309 avatar Dec 09 '25 08:12 mcmah309