Skip to content

RFC: Support for hashing more types of identifiers #390

@devongovett

Description

@devongovett

Hello! I maintain Lightning CSS, a CSS processor which implements CSS modules. Since the CSS modules "spec" was last updated, there have been many new features added to CSS itself, which I think could be affected by CSS modules hashing. I'd like to implement some of this in Lightning CSS, but want to get some consensus on the syntax here so that it remains interoperable between tools.

<custom-ident>

The CSS specification defines the <custom-ident> type for author-defined identifiers. This is used for keyframe animation names and the animation-name property, CSS grid line names, @container names, @counter-style names, and probably more future features. At the moment, CSS modules officially mentioned @keyframes, but not these other features. I think all <custom-ident> values across all features should be hashed consistently.

This has been implemented in Lightning CSS (and perhaps some other implementations) for a while, but I think it would be good if we could update the spec to define it in terms of the <custom-ident> type so that future additions to CSS automatically get hashed.

<dashed-ident>

The CSS spec also defines the <dashed-ident> type for cases where either a spec-defined or author defined identifier may be present. The leading -- allows these to be distinguished. This is most commonly used for CSS variables, but it is also used for other features like the @font-palette-values, @color-profile, and @custom-media rules, as well as other upcoming specs.

Lightning CSS currently implements support for local (hashed) CSS variables and dashed idents under an opt-in flag. I think this feature should also exist in the CSS modules spec so that it can be interoperable between tools. It seems to be in-line with the general CSS modules philosophy to scope all identifiers local to the file by default.

I believe there was a plan for webpack to also implement this at some point (according to @sokra), though I'm not sure what the current status of that is.

Accessing globals and dependencies

One problem with hashing custom and dashed idents is that there isn't a syntax to define or reference a global ident, or reference a <custom-ident> or <dashed-ident> in a different file. With selectors, the :global pseudo class can be used to define a global class or id, and the composes property supports referencing classes defined in another file or globally with the composes: name from "..." syntax. But with general custom or dashed idents this is not currently possible.

The spec currently mentions using :global(xxx) to define a global @keyframes rule. I think this syntax is kinda weird because it is not a selector, so the pseudo-class like syntax with the colon is out of place. It would also require changing the parsing of that rule from the standard CSS syntax. There is also no way to reference a hashed animation-name from a different file at the moment.

The CSS variables implementation in Lightning CSS currently allows referencing a variable defined in a different file using a syntax similar to composesvar(--foo from "./bar.css") or var(--foo from global). However, there is currently no way to define a global custom property within a CSS module file, or update the value of a custom property defined in a different file. The --foo from global: "value"; syntax seems pretty strange for that, and also violates the CSS spec which requires property names to be valid identifiers.

Proposal

Defining a global identifier

I propose adding an @global at-rule, which would make all selectors and identifiers defined within it global (non-hashed). This would enable defining global identifiers using other nested at-rules without changing their syntax.

@global {
  @counter-style circles {
    symbols: Ⓐ Ⓑ Ⓒ;
  }

  @keyframes fade {
    /* ... */
  }

  @custom-media --modern (color), (hover);
}

This could also be used to define global custom properties or selectors. Everything inside the @global rule would become global.

@global {
  .foo {
    --global-var: red;
  }
}

In this case, both .foo and --global-var would not be hashed. With CSS nesting, you could also define only the --global-var as global and keep .foo hashed.

.foo {
  @global {
    --global-var: red;
  }
}

I think this should only affect declarations and not references. Referencing another value within an @global rule should still reference a local (hashed) name.

@global {
  .foo {
    animation-name: fade;
  }
}

In this case, fade would be hashed, but .foo would not. To reference a global name or an identifier from a different file see below.

Referencing an identifier

To reference a <custom-ident> or <dashed-ident> from a different file, or from the global namespace, I propose adding an import() function. This would accept syntax similar to the composes property to define where to import from.

ul {
  list-style: import(circles from "./some-file.css");
  animation-name: import(fade from global);
  color: var(import(--accent-color from "./vars.css"));
}

The reason this is a function rather than simply inline (e.g. animation-name: fade from global) is to disambiguate cases where from and global could be valid identifiers as well, for example if a property accepted a space separated list.

We could also consider keeping var(--accent-color from "./vars.css") as a shortcut, but import() is a more general solution that works in more places.

Re-defining custom properties from a different file

Since the CSS spec requires that all declarations have property names that are identifiers in order to parse, we cannot use import() in a property name. Defining a global custom property can be achieved as described above, but re-defining or updating the value of a custom property from a different file is not possible.

To solve this, we could introduce an @with rule to "import" an identifier name from another file so it can be referenced as if it were local.

@with --accent-color from "./vars.css" {
  .foo {
    --accent-color: purple;
  }
}

This would cause the reference to --accent-color to be hashed as a dependency of "./vars.css".

This could also be used as an alternative to an inline import() call as described above. Here is the same example rewritten to use @with:

@with 
  circles from "./some-file.css",
  fade from global,
  --accent-color from "./vars.css"
{
  ul {
    list-style: circles;
    animation-name: fade;
    color: var(--accent-color);
  }
}

This is sort of similar to @value but with a couple very important differences:

  1. It only allows importing identifier names, not arbitrary values. This makes it much easier to parse, because it doesn't affect every single property value, only places where <custom-ident> or <dashed-ident> are accepted.
  2. It is scoped to a block. This also makes it much easier to replace during parsing, and easier to control where the definitions will apply rather than the whole file.

Grammar

This is the formal grammar proposed above, using the value definition syntax from the CSS specification.

@global {
  <style-block>
}

<css-module-reference> = <custom-or-dashed-ident> from <css-module-namespace>
<custom-or-dashed-ident> = <custom-ident> | <dashed-ident>
<css-module-namespace> = global | <string>

<import> = import(<css-module-reference>)

<css-module-reference-list> = <custom-or-dashed-ident># from <css-module-namespace>

@with <css-module-reference-list># {
  <rule-list>
}

Within a CSS module file, <custom-ident> and <dashed-ident> would also change their grammar to support import().

<css-module-custom-ident> = <custom-ident> | <import>
<css-module-dashed-ident> = <dashed-ident> | <import>

Conclusion

I think these additions to CSS modules could help it support modern CSS features like variables, animations, custom media queries, and more. I'm hoping we can get some consensus on the syntax here so that it remains interoperable between different tools. I'd love to hear your feedback!

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