Overriding a large number of !default values with the @use rule in Sass/Scss

Viewed 2195

I've got an SCSS module with a whole lot of overridable !default variables, and I'm trying to make use of it in a project using the new @use rule instead of the to-be-deprecated @import. Overriding a large number of variables used to be pretty straightforward. As long as I imported my variables before importing the module they would take precedence over the !default values.


Now, if I try to @use the module in the global namespace like so:

$h1-font-size: 40px;

@use 'my-module' as *;

I get an error - SassError: This module and the new module both define a variable named "$h1-font-size".


Apparently the proper way to do this is to use the with keyword like so

@use 'my-module' with (
    $h1-font-size: 40px
);

But now my $h1-font-size variable isn't available to other parts of my code. It's only scope is in overriding the value in the module. If I tried to assign it to something else, I'd get an Undefined variable error.


So that leaves me doing this

$h1-font-size: 40px;

@use 'my-module' with (
    $h1-font-size: $h1-font-size
);

.foo {
    font-size: $h1-font-size;
}

This is the exact effect I want - a variable that overrides my module and is still available for use elsewhere. However this syntax is cumbersome for passing a large number of overrides. If I want to pass say 30 overrides, I need 30 variable definitions and then 30 lines within the with block to explicitly pass in 30 values.


Update / Edit

The answer from @Miriam Suzanne below makes it apparent I have oversimplified my use case for the purpose of asking a question.

Our shop has a set of mixins and base styles we use on many projects and I have been attempting to turn them into a reusable module to reduce the copy/pasting. Something like Bootstrap, with a lot of overridable !default values. Very much a work in progress, but the module itself can be seen here: Derekstrap.

This module is being developed in tandem with the first project we're actually using this on. The !default values are all in the module, and I would like to override them in my project.

Right now I have code in my main.scss (the primary entry point for stylesheets) that looks like this.

$base-font-family: 'Raleway', Tacoma, sans-serif;
$base-line-height: 1.8;

@use '~@evanshunt/derekstrap/index' with (
    $base-font-family: $base-font-family,
    $base-line-height: $base-line-height
);

I like to structure my scss files pretty granularly so ideally those variables would actually live in another file, and also be available to other files in the project.

If I do something like the following, where both my variables file and the module have a $base-font-family variable defined, it compiles but by variables don't override the module.

@use 'variables' as *;
@use '~@evanshunt/derekstrap/index' as *;

I suspect at this point the answer is that I need to add @use and/or @forward statements to my variables file(s), but it's breaking my brain a bit to figure out if I'll be able to successfully override different parts of the module in different parts of my project code. The suggestion above of just one variables file is also a bit of a simplification. How important will the order of the @use statements in my main.scss become if I am modifying the same namespace across the files being @used?


I get the purpose of scoping and passing values explicitly but I'd love some sort of shorthand or trick to accomplish this without writing so much extra code. As mentioned in my Sass feature request ticket I was hoping for a simple shorthand for passing one context into another like:

@use 'variables' as variables;
@use 'module' with variables;

This doesn't seem possible and I'm wondering if I need to completely rethink the way I structure SCSS files to achieve what I want. What can I do here?

1 Answers

I'm not entirely clear what you mean when you say the variable "isn't available to other parts of my code". The entire purpose of @use is to make those variables available – but inside a namespace. So you can do:

@use 'module' with (
    $h1-font-size: 40px
);

.foo {
    font-size: module.$h1-font-size;
}

or remove the namespace using as *:

@use 'module' as * with (
    $h1-font-size: 40px
);

.foo {
    font-size: $h1-font-size;
}

or forward the module while also adding configuration, to use in other files:

@forward 'module' with (
    $h1-font-size: 40px !default
);

or both at once:

@forward 'module' with (
    $h1-font-size: 40px !default
);

@use 'module';

.foo {
    font-size: module.$h1-font-size;
}

I think that likely covers all the potential use-cases you might have for defining the same variable again?


In response to updates on the question…

Using the module syntax means that there is no global scope across files. That means every individual file needs to explicitly @use any-and-all required modules for that file. That means:

  • You will need to @use '~@evanshunt/derekstrap/index' (or similar) at the top of every single file that should have access to Derekstrap mixins, variables, or functions
  • Since index files are imported automatically, and default namespaces are generated based on the path, I'd probably @use '~@evanshunt/derekstrap' instead…
  • Namespaces are always local to the importing file, so you can @use '~@evanshunt/derekstrap' in one place (creating a derekstrap namespace), and @use '~@evanshunt/derekstrap' as * in another (removing the namespace), and still @use '~@evanshunt/derekstrap' as derek in a third place.

The goal of !default "configurable" variables is that they can be pre-configured before any compilation. That has to happen "first time" a module is used. If you want to do that in a file all to itself, you can:

// _config.scss
@use '~@evanshunt/derekstrap' with (
  $h1-font-size: 40px,
);

// main.scss
@use 'config'; // keep this path first...
@use 'anything-else';

// _anything-else.scss
@use '~@evanshunt/derekstrap';
// derekstrap.$h1-font-size will already be configured by config.scss

Those values can only be pre-configured once - meaning the compilation will start with the configuration value. But you can update those variables any time in any file:

// _anything-else.scss
@use '~@evanshunt/derekstrap';

derekstrap.$h1-font-size: 32px;
// The $h1-font-size now has a value of 32px
// for any file to uses derekstrap later in the compilation

On my site, I use a combination of both approaches - just inside my one color config file:

https://github.com/mirisuzanne/mia/blob/main/src/scss/config/_color.scss

I have the @use … with pattern for configuring cascading-color-systems, but I also use accoutrement tools without configuration, and then override tools.$colors.

You can get quite a bit more complex by adding in layers of forwarding, but this should get you started. Remember:

  • The variable $h1-font-size exists inside the '~@evanshunt/derekstrap' module.
  • That module can used multiple times, and given a different namespace each time. The module is always the same module, but namespaces are local.
  • Pre-compile "Configuration" can only happen the first time a module is used, but you can continue to override those variables at any time.

Much like before, there is only one Derekstrap $h1-font-size variable, and it takes on different values over the course of compilation. what's new with modules is only that you need to explicitly @use Derekstrap in every file that wants access to that variable.

I understand it can take some getting used to, but in the end I've found my code is much more organized. You might want to set up a test project and play around, until the mental model makes more sense.

Related