I'm trying to add theming support to my react web app. I had used CSS variables extensively before. One problem I wanted to solve was that the name of the variables was getting duplicated across every theme and some vars would go missing in one theme vs the other. So I wanted to use SCSS to do some CSS variable generation so that the attributes of theme can be defined in one place. SCSS just reads the map and generates CSS variables for every supported theme. My project is split into different component packages and I would like these variables defined per component and not in one file. Also wanted to avoid name collisions for CSS variables across components and want compilation to fail if someone uses a CSS variable that is not defined. App uses webpack 5 and sass-loader.
This is my themes.modules.scss file.
@use "sass:map";
@use "sass:meta";
$-theme-vars: ();
$-app-level-theme-vars: null !default;
$supported-themes: (
"light",
"dark"
);
// Public
@mixin generate-theme-vars($theme-map) {
// Iterate through the theme vars and check for key collisions
@each $key, $value in $theme-map {
@if map-has-key($-theme-vars, $key) {
@error "ERROR: #{$key} is already defined in another theme map";
}
}
// Merge the theme map with global theme map
$-theme-vars: map.deep-merge($-theme-vars, $theme-map) !global;
// Generate the theme classes
@each $key in $supported-themes {
@include -generate-vars-for-theme($theme-map, $key);
}
}
@mixin set-app-level-theme-vars($theme-map) {
@if $-app-level-theme-vars == null {
$-app-level-theme-vars: map.merge((), $theme-map) !global;
} @else {
@error "ERROR: App level theme variables have already been set";
}
}
// If the property doesn't exist in the theme maps, throw an error
@function get-var($property, $theme-map: ()) {
// Merge maps with preference to the global map
$merged-theme-map: map.merge($-app-level-theme-vars, $theme-map);
$ret: -map-get-strict($merged-theme-map, $property);
@return "var(--#{$property}";
}
// Private
@mixin -generate-vars-for-theme($theme-map, $theme-name) {
html[theme="#{$theme-name}"] {
@each $key, $value in $theme-map {
--#{$key}: #{-map-get-strict($value, $theme-name)};
}
}
}
@function -map-get-strict($map, $key) {
@if map-has-key($map, $key) {
@return map-get($map, $key);
}
@else {
@error "ERROR: #{$key} is not a valid theme variable";
}
}
I have app.module.scss where it is used like this:
@use "../../styles/themes.module" as themes;
$-theme-vars: (
"app-background": (
"light": #f5f5f5,
"dark": #121212,
),
"app-accent-color": (
"light": #007bff,
"dark": #007bff,
),
);
// Set the app level theme variables. Should be set only from here and only once
@include themes.set-app-level-theme-vars($-theme-vars);
// Generate the theme classes
@include themes.generate-theme-vars($-theme-vars);
When another component tries to do the same thing, $-app-level-theme-vars goes back to null.
@use "sass:meta";
@use "../../../styles/themes.module" as themes;
$-theme-vars: (
"pin-view-background" : (
"light": #f5f5f5,
"dark": #1e1e1e
)
);
@include themes.generate-theme-vars($-theme-vars);
.container {
display: flex;
background-color: themes.get-var("app-background");
}
I want anyone importing themes.module.scss to basically maintain state so that I can do:
- Collisions checks at build time
- CSS variable names checks at build time. Only allow your component vars and the app level component vars.
Why is the state getting reset every time I import it? I have also tried using @use "" as *. I have tried forwarding those vars. Nothing works. Somehow, sass-loader is not able to figure out that these are the same modules?