144
votes

I'm trying to combine the use of a Sass variable with @media queries as follows:

$base_width:1160px;

@media screen and (max-width: 1170px) {$base_width: 960px;}
@media screen and (min-width: 1171px) {$base_width: 1160px;}

$base_width is then defined at various points in the stylesheet width percentage-based measurements to produce fluid layouts.

When I do this, the variable seems to be recognized properly but the conditions for the media query are not. For example, the above code produces an 1160px layout regardless of screen width. If I flip-flop the @media statements like so:

@media screen and (min-width: 1171px) {$base_width: 1160px;}
@media screen and (max-width: 1170px) {$base_width: 960px;}

It produces a 960px layout, again regardless of screen width. Also note that if I remove the first line of $base_width: 1160px; it returns an error for an undefined variable. Any ideas what I'm missing?

8

8 Answers

111
votes

This is simply not possible. Since the trigger @media screen and (max-width: 1170px) happens on the client-side.

Achieving your expected result would only be possible if SASS grabbed all rules and properties in your stylesheet containing your $base_width variable and copied/changed them accordingly.

Since it won't work automatically you could do it by hand like this:

@media screen and (max-width: 1170px)
      $base_width: 960px // you need to indent it to (re)set it just within this media-query
      // now you copy all the css rules/properties that contain or are relative to $base_width e.g.
      #wrapper
          width: $base_width
          ...

@media screen and (min-width: 1171px)
    $base_width: 1160px
      #wrapper
          width: $base_width
          ...

This is not really DRY but the best you can do.

If the changes are the same every time you could also prepare a mixin containing all the changing values, so you wouldn't need to repeat it. Additionally you can try to combine the mixin with specific changes. Like:

@media screen and (min-width: 1171px)
    +base_width_changes(1160px)
    #width-1171-specific-element // additional specific changes, that aren't in the mixin
        display: block

And the Mixin would look like this

=base_width_changes($base_width)
    #wrapper
        width: $base_width
69
votes

Similar to Philipp Zedler's answer, you can do it with a mixin. That lets you have everything in a single file if you want.

    @mixin styling($base-width) {
        // your SCSS here, e.g.
        #Contents {
            width: $base-width;
        }
    }

    @media screen and (max-width: 1170px) {
        @include styling($base-width: 960px);
    }
    @media screen and (min-width: 1171px) {
        @include styling($base-width: 1160px);
    }
14
votes

This isn't possible with SASS, but it is possible with CSS variables (or CSS custom properties). The only drawback is browser support – but there's actually a PostCSS plugin - postcss-css-variables - that "flattens" the use of CSS variables (which gives you support for older browsers, too).

The following example works great with SASS (and with postcss-css-variables you get support for older browsers too).

$mq-laptop: 1440px;
$mq-desktop: 1680px;

:root {
    --font-size-regular: 14px;
    --gutter: 1rem;
}

// The fact that we have to use a `max-width` media query here, so as to not
// overlap with the next media query, is a quirk of postcss-css-variables
@media (min-width: $mq-laptop) and (max-width: $mq-desktop - 1px) {
    :root {
        --font-size-regular: 16px;
        --gutter: 1.5rem;
    }
}

@media (min-width: $mq-desktop) {
    :root {
        --font-size-regular: 18px;
        --gutter: 1.75rem;
    }
}

.my-element {
    font-size: var(--font-size-regular);
    padding: 0 calc(var(--gutter) / 2);
}

This would result in the following CSS. The repetitive media queries will increase the file size, but I have found that the increase is usually negligible once the web server applies gzip (which it will usually do automatically).

.my-element {
  font-size: 14px;
  padding: 0 calc(1rem / 2);
}
@media (min-width: 1680px) {
  .my-element {
  padding: 0 calc(1.75rem / 2);
  }
}
@media (min-width: 1440px) and (max-width: 1679px) {
  .my-element {
  padding: 0 calc(1.5rem / 2);
  }
}
@media (min-width: 1680px) {
  .my-element {
  font-size: 18px;
  }
}
@media (min-width: 1440px) and (max-width: 1679px) {
  .my-element {
  font-size: 16px;
  }
}
10
votes

Edit: Please do not use this solution. The answer by ronen is much better.

As a DRY solution, you can use the @import statement inside a media query, e.g. like this.

@media screen and (max-width: 1170px) {
    $base_width: 960px;
    @import "responsive_elements";
}
@media screen and (min-width: 1171px) {
    $base_width: 1160px;
    @import "responsive_elements";
}

You define all responsive elements in the file included using the variables defined in the media query. So, all you need to repeat is the import statement.

9
votes

With @ronen's great answer and a map, there's some real power available:

    @mixin styling($map) {
        .myDiv {
            background: map-get($map, 'foo');
            font-size: map-get($map, 'bar');
        }
    }

    @media (min-height: 500px) {
        @include styling((
            foo: green,
            bar: 50px
        ));
    }

    @media (min-height: 1000px) {
        @include styling((
            foo: red,
            bar: 100px
        ));
    }

It's now possible to have lots more DRY media queries targeting .myDiv with a bunch of different values.


Map docs: https://sass-lang.com/documentation/functions/map

Example map usage: https://www.sitepoint.com/using-sass-maps/

6
votes

I had the same problem.

The $menu-width variable should be 240px on the mobile view @media only screen and (max-width : 768px) and 340px on the desktop view.

So i have simply created two variables:

$menu-width: 340px;
$menu-mobile-width: 240px;

And here is how i have used it:

.menu {
    width: $menu-width;
    @media only screen and (max-width : 768px) {
      width: $menu-mobile-width;
    }
}
1
votes

Two recommendations

1 Write your "default" CSS statements to be for small screens and only use media queries for larger screens. There's usually no need for a max-width media query.

Example (assuming the element has class "container")

@mixin min-width($width) {
  @media screen and (max-width: $width) {
    @content;
  }
}

.container {
  width: 960px;

  @include min-width(1170px) {
    width: 1160px;
  }
}

2 Use CSS variables to solve the problem, if you can.

@mixin min-width($width) {
  @media screen and (max-width: $width) {
    @content;
  }
}

:root {
  --container-width: 960px;
  @include min-width(1170px) {
    --container-width: 1160px;
  }
}

.container {
  width: var(--container-width);
}

Note:

Since it will have the width of 1160px when the window has a width of 1170px, it may be better to use a width of 100% and max-width of 1160px, and the parent element might have a horizontal padding of 5px, as long as the box-sizing property is set to border-box. There are a lot of ways to solve the problem. If the parent is not a flex or grid container you might use .container { margin: auto }.

0
votes

This is also possible with %placeholders.

%placeholders can be wrapped in media queries. So you could set up multiple variables to use at different screen sizes, and then the placeholders would automagically pre-process accordingly. I'm using some mixins to shorten my media query declarations here also.

In your _vars.scss file:

    $width-1: 960px;
    $width-2: 1160px;

In your _placeholders.scss file:

    %variable-site-width                    { 
        @media screen and (max-width: 1170px)       { width: $width-1; }
        @media screen and (min-width: 1171px)       { width: $width-2; }
    }

In your page.scss file:

    .wrapper.     { @extend %variable-site-width; background: red; etc... }

And this will compile to something similar to:

    @media screen and (max-width: 1170px) { 
        .wrapper { width: 960px; }
    }
    @media screen and (min-width: 1171px) { 
        .wrapper { width: 1160px; }
    } 

Voila!

I use this technique extensively for things like variable font sizes and a raft of other things.