Skip to content

Behaviour of composed classes - array or space separated strings? #417

@mrginglymus

Description

@mrginglymus

The exact structure of the javascript generated from css modules by build tools appears to be somewhat underspecified. Some tools used named exports, some a default export of an object. Many tools offer the ability to transform case to camel or similar. None of these appear to be defined in the spec.

I've found a third bit of variability which is causing me some headache - how to handle composed classes.

Given the CSS file:

.foo {
  color: red;
}

.bar {
  composes: foo;
  background-color: blue;
}

Most tools will output something like:

export default {
  foo: 'foo_abc123',
  bar: 'bar_xyz789 foo_abc123'
}

However, this does not play nicely with direct DOM operations.

Firstly, manipulating an HTMLElement's class list (via the DOMTokenList api) requires a list of tokens, each of which must not contain a space characters. As the type information of the above does not contain any clue as to whether the contents do contain a space, instead of being able to just:

$0.classList.add(classes.bar);

one is forced to defensively do something like:

$0.classList.add(...classes.bar.split(' '));

Secondly, attempting to turn them into valid selectors again only works trivially if you know whether or not they contain spaces -

document.querySelector(`.${classes.bar}`);

does not work, but

document.querySelector(`.${classes.bar.replaceAll(' ', '.')}`);

does.


I do not expect the spec to suddenly impose specific behaviours on implementers - that horse has well and truly bolted. However, I think it would be of great benefit to document what output options might be considered valid.

I raised this issue with the most excel lent vite-css-modules (privatenumber/vite-css-modules#19) and was quite reasonably told that if it wasn't in the spec it couldn't be unilaterally added to that package. However, as mentioned, also not mentioned in the spec are the export format or case-changing capabilities, both of which have many interpretations.


n.b. in the mean time, for anyone else concerned with this I am currently playing with the horrific workaround of emitting a type for my stub files that looks like this:

declare const styles {
  foo: string;
  bar: {
    split(sep: " "): string[];
  }
}

which prevents one from accidentally using it as a string. Horrible, but effective.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions