Cross-Media Query @extend Directives in Sass

Share this article

If you read my previous article on Sass’s @extend directive or if you have pretty solid knowledge about the way Sass works, you may be aware that you cannot do this:

%example {
  color: blue;
  font-weight: bold;
  font-size: 2em;
}

@media (max-width: 800px) {
  .generic-class {
    @extend %example;
  }
}

What’s happening here is we’re trying to extend a placeholder inside a media query from another scope (in this case, the root). It just doesn’t work, and if you try it you’ll get the following error message:

You may not @extend an outer selector from within @media.
You may only @extend selectors within the same directive.

That sucks, doesn’t it? So the other day, I rolled up my sleeves and dug deep into the code to find a way to create an @extend directive that works across media queries. It was an amazing journey and eventually I ended up with a not-so-terrible solution. However keep in my mind that this technique is basically a hack, so it’s mostly experimental. But if you can’t wait for a native Sass solution, I suppose you could give this a try.

How Does it Work?

This is a very complex solution, so you may have to go over this a few times. First, it relies on knowing the existing @media scopes from the beginning. Generally, this shouldn’t be a problem because our code will usually have a list of named breakpoints. Next, the key is to create the same placeholder in every scope so no matter what scope we’re currently inside, we can use the placeholder. Then, the tricky part is to know which scope we’re currently in. For example: Are we at the root level? Are we inside a small breakpoint? A large one? Not a simple task.

Because this is quite complicated, the resulting Sass code is pretty heavy. That being said, I have a motto:

It doesn’t matter how complicated the core code is, as long as the API is simple.

So that’s really what I was aiming for: a simple API so that the developer doesn’t have to do a ton of complicated stuff just to extend a placeholder from a media query.

Before getting to the step-by-step build of the code that our API will be based on, let’s look at the API itself, comparing it to what we might normally do with a placeholder:

/* Regular single-scope placeholder */
%my-placeholder {
  property: value;
}

/* Enhanced multi-scope placeholder */
@include placeholder(my-placeholder) {
  property: value;
}

While the multi-scope version (2nd example above) is a little longer to type than the standard placeholder syntax, it is just as easy to understand. Also, the fact that it’s slightly longer shouldn’t matter much since you don’t create placeholders every two minutes. To shorten this, you could rename it to ph() or even p(), or create aliases for it.

Now that we’ve defined our placeholder, we can extend it. Here is how we would do it, with an example first of how you would normally do it without the cross-scope benefits:

/* Regular @extend, with cross-scope limitations */
@extend %my-placeholder;

/* Enhanced cross-scope @extend */
@include _(my-placeholder);

Okay, now I know it looks weird, but before puking, hear me out: Since we need to pass the name of the placeholder as a parameter to a mixin, I gave the mixin (which you’ll see defined later) a very short name (in this case, one character). This keeps it brief and easy to use.

At first, I wanted to use %() to be consistent with the more common placeholder syntax. However, doing this would require that the % character be escaped with a backslash \ (e.g. \%(my-placeholder)). This would defeat the purpose and make it a pain to use. Hence the underscore: _().

Note: remember that Sass considers underscores and hyphens the same. So you could use @include -(my-placeholder) instead. Otherwise, feel free to create the alias you want — extend(), gloubiboulga(), god-i-love-sass-so-much(), or whatever.

In the end, I think the API is not that bad. It may look odd at first but I think you’ll get used to it pretty quickly so it should be good enough.

Writing the Library for our API

Let’s sum up what we need in order to make the whole thing work:

  1. A couple of settings, including a list of breakpoints
  2. A mixin to handle media queries (let’s call it breakpoint())
  3. A mixin to generate a placeholder (placeholder())
  4. A mixin to extend a placeholder (_())

From this point on, you can copy and paste each of the code examples into a Sass file, in the order they appear (or into a tool like SassMeister), to slowly build the code that’s used by the API discussed in the previous sections. You can also view the final code in a Gist on SassMeister.

Set Up and Configuration

First, the configuration. As I said, from the beginning we need to know the different breakpoints (which are our media query scopes). For this, we can use a simple map associating a keyword with a max-width value. Like this:

$breakpoints: (
  "small"  : 600px,
  "medium" : 900px,
  "large"  : 1200px
);

Naturally, you can pick the names you want and the width values you want. I decided to keep things simple with 3 very basic keywords and 3 standard widths for the demo, but feel free to do as you want.

Next we need a variable to store the current breakpoint (or scope) we’re in. Let’s call it $current-breakpoint. We also need to give it a default value, which can be anything. I decided to call it root, because we start at root level.

$default-breakpoint: root;
    $current-breakpoint: $default-breakpoint;

Lastly, we need a variable to store the names of all generated placeholders. Don’t worry, this variable will be auto-filled. At first, no placeholder exists so we initialize it as an empty list.

$placeholders: ();

I think we’re good with the set up!

The Media Query Mixin

The media query mixin is meant to open a new scope. This means instead of manually opening a @media scope with @media (min-width: 900px) {}, we instead include the breakpoint mixin by passing it a keyword (e.g. @include breakpoint(small) {}). Of course, the keyword has to be defined in the $breakpoint map (that’s the whole point of it). Then, the mixin opens the right scope for us.

Where it’s getting interesting is this mixin also updates the $current-breakpoint variable to keep track of the current scope. Here is the code, with comments to make each section clear:

@mixin breakpoint($breakpoint) {
      // Get the width from the keyword `$breakpoint`
      // Or `null` if the keyword doesn't exist in `$breakpoints` map
      $value: map-get($breakpoints, $breakpoint);

  // If `$breakpoint` exists as a key in `$breakpoints`
  @if $value != null {
        // Update `$current-breakpoint`
        $current-breakpoint: $breakpoint !global;

    // Open a media query block
    @media (min-width: $value) {
      // Let the user dump content
      @content;
    }

    // Then reset `$current-breakpoint` to `$default-breakpoint` (root)
    $current-breakpoint: $default-breakpoint !global;
  }

  // If `$breakpoint` doesn't exist in `$breakpoints`, 
  // Warn the user and do nothing
  @else {
    @warn "Invalid breakpoint `#{$breakpoint}`.";
  }
}

As you can see, right before dumping user content (@content), we update the $current-breakpoint variable with the keyword passed to the mixin (so in our case small, medium or large), and right after it’s dumped we reset it to root (i.e. our $default-breakpoint variable).

Note: the !global flag has been added to Sass 3.3 to make a distinction between defining a scoped variable and redefining a global variable. In our case, we are overriding $current-breakpoint, which is global.

The Mixin to Generate the Placeholder

Our mixin is fairly straightforward. However, note the one verification we make before doing anything: We only generate the placeholders if they do not exist yet. To make sure they don’t, we check the name passed to the mixin against the list of existing placeholders ($placeholders). If it doesn’t exist in the list yet, we add it and then generate the placeholders. This makes sure a placeholder cannot be generated twice, which would lead to an error.

@mixin placeholder($name) {
      // If placeholder doesn't exist yet in `$placeholders` list
      @if not index($placeholders, $name) {
        // Store its name
        $placeholders: append($placeholders, $name) !global;

    // At root level
    @at-root {
      // Looping through `$breakpoints`
      @each $breakpoint, $value in $breakpoints {
            // Opening a media query block
            @media (min-width: $value) {
              // Generating a placeholder
              // Called $name-$breakpoint 
              %#{$name}-#{$breakpoint} {
            @content;
          }
        }
      }

      // And dumping a placeholder out of any media query as well
      // so basically at root level
      %#{$name}-#{$default-breakpoint} {
        @content;
      }
    }
  }

  // If placeholder already exists, just warn the user
  @else {
    @warn "Placeholder `#{$name}` already exists.";
  }
}

Extending the Placeholder

All we have left is our short mixin to extend a placeholder no matter the media block we’re in:

@mixin _($name) {
      @extend %#{$name}-#{$current-breakpoint} !optional;
}

This is where we actually use the $current-breakpoint value, to extend the accurate placeholder from the scope we’re in. The !optional flag< ?a> is here only as a security measure. If, for any reason, the placeholder doesn’t exist (which shouldn’t happen, but you never know), the @extend won’t crash.

A Working Example

Let’s create a simple example as a proof of concept: A clearfix placeholder. We’ll keep it simple; it dumps a clear: both for simple float clearing, and overflow: hidden for inner float clearing. That’s definitely not the best clearfix method; we’re just using it for our purposes here.

First, we need to create the placeholder:

@include placeholder('clear') {
  clear: both;
  overflow: hidden;
}

And now we can use it:

.a {
  @include _(clear);
}

.b {
  @include _(clear);
}

.c {
  @include breakpoint(medium) {
    @include _(clear);
  }
}

@include breakpoint(medium) {
  .d {
    @include _(clear);
  }
}

.e {
  @include _(clear);

  @include breakpoint(large) {
    @include _(clear);
  }
}

This will result in the following CSS:

@media (min-width: 900px) {
  .c, .d {
    clear: both;
    overflow: hidden;
  }
}
@media (min-width: 1200px) {
  .e {
    clear: both;
    overflow: hidden;
  }
}
.a, .b, .e {
  clear: both;
  overflow: hidden;
}

Not bad, is it?

Final thoughts

So in the end, we managed to create a mixin able to extend a placeholder no matter the @media block we’re in, resulting in some neat and optimized CSS output. Also, in my opinion, the API is not much more complicated than the usual %placeholder {}/@extend %placeholder workflow.

Now, is it really useful? If you ask me, I’m not sure. As far as I’m concerned, I have yet to face a case where I truly needed a cross-scope @extend. I think it happened to me only once and I managed to work around the issue without much difficulty. I feel like mixins and placeholders usually define the core of an element, which means they should be applied at the root level and not at a given breakpoint precisely – at least, that’s how I do it in my code.

Besides that, using a mixin when stuck in a @media block with no access to root placeholders is probably way simpler. Also, gzip aggressively crushes repeated strings so I’m not sure using mixins instead of placeholders is that bad an idea when we’re concerned about final file size, but that’s another story.

In any case, this is a fun experiment to play with. Hope you liked it! By the way, you can play with the code at SassMeister!

Play with this gist on SassMeister.

Frequently Asked Questions on Cross-Media Query Extend in Sass

What is the purpose of using cross-media query extend in Sass?

Cross-media query extend in Sass is a powerful feature that allows developers to reuse and extend existing styles in different media queries. This helps in maintaining a clean, DRY (Don’t Repeat Yourself) codebase. It’s particularly useful when you want to apply the same styles to different elements at different breakpoints, without having to rewrite the same code.

Why can’t I extend selectors from within media queries in Sass?

Extending selectors from within media queries in Sass can lead to unexpected results and can increase the size of the generated CSS. This is because Sass extends the selector by duplicating the rules, which can lead to unnecessary repetition in the output CSS. Therefore, it’s recommended to use mixins or include the styles directly within the media query.

How can I share CSS code between a selector and a media query?

You can share CSS code between a selector and a media query by using Sass mixins. A mixin is a block of code that can be reused throughout the stylesheet. You can define a mixin and then include it in your selector and within your media query. This way, you can maintain DRY code and avoid duplicating styles.

What are the potential issues with extending in Sass?

While extending in Sass can be powerful, it can also lead to potential issues. One of the main issues is that it can lead to bloated CSS output, especially when extending complex selectors. It can also lead to specificity issues, where the extended styles might not apply as expected due to the increased specificity of the extended selector.

How can I avoid mess while extending in Sass?

To avoid mess while extending in Sass, it’s recommended to use it sparingly and wisely. Avoid extending complex selectors, as it can lead to bloated CSS output. Instead, consider using mixins or including the styles directly. Also, keep your selectors as simple and as flat as possible to avoid specificity issues.

Can I use cross-media query extend in Sass for responsive design?

Yes, cross-media query extend in Sass can be used for responsive design. It allows you to apply different styles to different elements at different breakpoints, without having to rewrite the same code. However, it’s important to use it wisely to avoid bloated CSS output and specificity issues.

How does cross-media query extend in Sass improve code maintainability?

Cross-media query extend in Sass improves code maintainability by allowing you to reuse and extend existing styles. This helps in keeping your code DRY and organized. It also makes it easier to update or change styles, as you only need to update them in one place.

What is the difference between @extend and @mixin in Sass?

The @extend directive in Sass allows you to share a set of CSS properties from one selector to another. On the other hand, @mixin allows you to create reusable chunks of code. While they both help in keeping your code DRY, @mixin is generally preferred over @extend due to its flexibility and less potential for bloating the CSS output.

Can I extend multiple selectors in Sass?

Yes, you can extend multiple selectors in Sass. You can use the @extend directive with multiple selectors separated by a comma. However, keep in mind that this can increase the specificity of the extended selectors and can lead to bloated CSS output.

How can I debug issues with extending in Sass?

Debugging issues with extending in Sass can be done by inspecting the generated CSS. Check if the styles are being applied as expected and if the CSS output is not unnecessarily bloated. Also, consider using Sass’s debugging features such as @debug and @warn to output debugging information in the console.

Kitty GiraudelKitty Giraudel
View Author

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/they.

media queriessasssass extend
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form