Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Modular CSS

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I love that I can put my styles right inside the component. In products.vue, we render an element with a sidebar class... and then immediately - without going anywhere else - we're able to add the CSS for that. We can still have external CSS files with shared CSS, but for any styling that's specific to a component, it can live right there.

CSS Name Conflicts

But... we need to be careful. The class sidebar is a pretty generic name. If we accidentally add a sidebar class to any other component - or even to an element in our Twig layout - this CSS will affect it!

Find your browser, refresh, and open the HTML source. On top, here's our stylesheet: /build/products.css. Open that up. Yep! That's what I expected: a .sidebar class with the CSS from the component. It's easy to see that these styles would affect any element on the page with a sidebar class... whether we want it to or not.

Hello Modular CSS

To solve this problem, Vue, well really, the CSS world - because this is a generic CSS problem - has created two solutions: scoped CSS and modular CSS. They're... two slightly different ways to solve the same problem and you can use either inside of Vue. We're going to use modular CSS.

What does that mean? It means that whenever we have a style tag in a Vue component, we're going to include a special attribute called module.

... lines 1 - 71
<style lang="scss" module>
... lines 73 - 83
</style>

That's it. Back at your browser, leave the CSS file open, but close the HTML source and refresh the homepage. Ah! We lost our sidebar styling! It's a modular CSS "feature", I promise!

To see what's going on, go back to the tab with the CSS file and refresh. Woh. The class names changed: they're now .sidebar_ and then a random string. Back on the main tab, if we inspect element... the div still has the normal sidebar class.

Here's what's going on. When you add module to the style tag, Vue generates a random string that's specific to this component and adds that to the end of all class names. The great thing is that we can use generic class names like sidebar without worrying about affecting other parts of our page. Because... in the final CSS, the class name is really sidebar_ then that random string.

Of course, now that this is happening, we can't just say class="sidebar" in the template anymore. We need to somehow use the dynamic name - the one that includes the random string.

Rendering the Dynamic CSS Class

As soon as you add module to a style tag, Vue makes a new variable available in your template called $style, which is a map from the original class name - like sidebar - to the new dynamic name - like sidebar_abc123.

I'm going to delete the p-3 mb-5 classes temporarily... just to simplify.

Ok: we no longer want to set the class attribute to a simple string: it needs to be dynamic. And whenever an attribute needs to contain a dynamic value, we prefix it with :, which is really v-bind dressed up in its superhero costume.

Now, we're writing JavaScript. Use that new $style variable to say $style.sidebar.

<template>
... lines 2 - 4
<div :class="$style.sidebar">
... lines 6 - 53
</template>
... lines 55 - 85

Unfortunately, at this time, the Vue plugin in PhpStorm doesn't understand the $style variable, so it won't be much help here. But because we have a class called sidebar inside the style tag, we can say $style.sidebar.

Let's try it! Move over, refresh and... we're back! Our class renders with the dynamic name.

The Powerful class Attribute Syntaxes

Of course, it looks a little weird because our element is missing the two classes we removed. How can we add them back? We could do some ugly JavaScript, a plus, quote, space... but... come on! Vue almost always has a nicer way.

In fact, Vue has special treatment for the class attribute: instead of setting it to a string, you can also pass it an array. Now we can include $style.sidebar and the two static classes inside quotes: p-3 and mb-5.

<template>
... lines 2 - 4
<div :class="[$style.sidebar, 'p-3', 'mb-5']">
... lines 6 - 53
</template>
... lines 55 - 85

That should do it! Back at the browser... much better.

So... that's modular CSS! We're going to use it throughout the project and we'll learn a couple more tricks along the way.

Controlling the "ident" (modular CSS Class Name)

You may have noticed that, when you look at the DOM, it's not super clear which component the sidebar class is coming from: that random string doesn't tell us much. That's not a huge deal, but with a little config, we can make this friendlier in dev mode.

Open up webpack.config.js. It doesn't matter where, but down here after enableVueLoader(), I'm going to paste in some code. You can get this from the code block on this page.

... lines 1 - 8
Encore
... lines 10 - 66
// gives better module CSS naming in dev
.configureCssLoader((config) => {
if (!Encore.isProduction() && config.modules) {
config.modules.localIdentName = '[name]_[local]_[hash:base64:5]';
}
})
... lines 73 - 93

I admit, this is a bit low-level. Inside of Webpack, the css-loader is what's responsible for understanding and processing styles. This localIdentName is how the random string is generated when using modular CSS. This tell it to use the component name, then the class name - like sidebar - and then a random hash. And we're only doing this in dev mode because when we build for production, we don't care what our class names look like.

Tip

Actually, the [name] part will be the filename of the Vue component, not its name.

To make this take effect, at your terminal, hit Ctrl+C to stop Encore and then restart it:

yarn watch

Once that finishes, I'll move back to my browser. Refresh the CSS file first. Nice! The class name is products_sidebar_ random string. And when we try the real page, it works too.

Next, oh, we're going to try something that I'm so excited about. It's called "hot module replacement"... which is a pretty cool-sounding name for something even cooler: the ability to make a change to our Vue code and see it in our browser without - wait for it - reloading the page. Woh.

Leave a comment!

3
Login or Register to join the conversation
S-H Avatar
S-H Avatar S-H | posted 3 months ago | edited

If you have problems with the name part of the css class replace the config part in webpack.config.js with

.configureCssLoader((config) => {
        if (!Encore.isProduction() && config.modules) {
            config.localIdentName = '[name]_[local]_[hash:base64:5]';
        }
    })

It seems like the setting moved an level higher.

found in https://vue-loader.vuejs.org/guide/css-modules.html#usage
there you can see that modules and localIdentName are on the same level

Not testet in Vue3

EDIT: And don't forget to start yarn watch again ;)

1 Reply
Default user avatar
Default user avatar Kakashi | posted 1 year ago | edited

Hello,
I have a problem.
I'm trying to reproduce the work as shown but i'm facing an issue with the use of $style variable.
This is the error i'm having:

<blockquote>[Vue warn]: Property "$style" was accessed during render but is not defined on instance.
at <App>
[Vue warn]: Unhandled error during execution of render function
at <App>
Uncaught TypeError: Cannot read properties of undefined (reading 'sidebar')
</blockquote>

Here is my products.vue complete code


<template>
    <div class="container-fluid">
        <div class="row">
            <aside class="col-xs-12 col-3">
                <div :class="$style.sidebar">
                    <h5 class="text-center">
                        Categories
                    </h5>

                    <ul class="nav flex-column mb4">
                        <li class="nav-item">
                            <a
                                class="nav-link"
                                href="/"
                            >All Products</a>
                        </li>
                        <li class="nav-item">
                            <a
                                class="nav-link"
                                href="#"
                            >Category A</a>
                        </li>
                        <li class="nav-item">
                            <a
                                class="nav-link"
                                href="#"
                            >Category B</a>
                        </li>
                    </ul>
                </div>
            </aside>

            <div class="col-xs-12 col-9">
                <div class="row">
                    <div class="col-12">
                        <h1>
                            Products
                        </h1>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-6 mb-2 pb-2">
                        TODO - load some products!
                    </div>
                </div>

                <div class="row">
                    <legend-component :title="legend" />
                </div>
            </div>
        </div>
    </div>
</template>


<script>
import LegendComponent from '../components/legend';

export default {
    name: 'Products',
    components: {
        LegendComponent,
    },
    data() {
        return {
            legend: 'Shipping takes 10-12 weeks, and products probably won\'t work again',
        };
    },
};
</script>

<style lang="scss" module>
@import '../../scss/components/light-component';
.sidebar {
  @include light-component;
  ul {
    li a:hover {
      background: $blue-component-link-hover;
    }
  }
}
</style>

Reply

Hey @Kakashi!

Do the $style variable becomes available IF you're using "modular" CSS. So, that's where I looked first. And I noticed you have module="". In the tutorial, it's just &lt;style lang="scss" module&gt;. My first guess would be to tweak this part: I've never used module="", so I don't know if that actually works or not (it seems like it's not working!).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.0
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.1.0
        "eslint-plugin-import": "^2.19.1", // 2.20.2
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.5
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.11
        "vue-loader": "^15.9.1", // 15.9.2
        "vue-template-compiler": "^2.6.11", // 2.6.11
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}
userVoice