diff --git a/src/utils.js b/src/utils.js
index 218c0b73..dd40967a 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -49,6 +49,18 @@ const filenameReservedRegex = /[<>:"/\\|?*]/g;
 // eslint-disable-next-line no-control-regex
 const reControlChars = /[\u0000-\u001f\u0080-\u009f]/g;
 
+function escapeLocalident(localident) {
+  return cssesc(
+    localident
+      // For `[hash]` placeholder
+      .replace(/^((-?[0-9])|--)/, '_$1')
+      .replace(filenameReservedRegex, '-')
+      .replace(reControlChars, '-')
+      .replace(/\./g, '-'),
+    { isIdentifier: true }
+  );
+}
+
 function defaultGetLocalIdent(
   loaderContext,
   localIdentName,
@@ -60,19 +72,9 @@ function defaultGetLocalIdent(
   const request = normalizePath(path.relative(context, resourcePath));
 
   // eslint-disable-next-line no-param-reassign
-  options.content = `${hashPrefix + request}\x00${unescape(localName)}`;
+  options.content = `${hashPrefix + request}\x00${localName}`;
 
-  // Using `[path]` placeholder outputs `/` we need escape their
-  // Also directories can contains invalid characters for css we need escape their too
-  return cssesc(
-    interpolateName(loaderContext, localIdentName, options)
-      // For `[hash]` placeholder
-      .replace(/^((-?[0-9])|--)/, '_$1')
-      .replace(filenameReservedRegex, '-')
-      .replace(reControlChars, '-')
-      .replace(/\./g, '-'),
-    { isIdentifier: true }
-  ).replace(/\\\[local\\]/gi, localName);
+  return interpolateName(loaderContext, localIdentName, options);
 }
 
 function normalizeUrl(url, isStringValue) {
@@ -143,7 +145,8 @@ function getModulesOptions(rawOptions, loaderContext) {
     localIdentHashPrefix: '',
     // eslint-disable-next-line no-undefined
     localIdentRegExp: undefined,
-    getLocalIdent: defaultGetLocalIdent,
+    // eslint-disable-next-line no-undefined
+    getLocalIdent: undefined,
     namedExport: false,
     exportLocalsConvention: 'asIs',
     exportOnlyLocals: false,
@@ -281,32 +284,42 @@ function getModulesPlugins(options, loaderContext) {
       extractImports(),
       modulesScope({
         generateScopedName(exportName) {
-          let localIdent = getLocalIdent(
-            loaderContext,
-            localIdentName,
-            exportName,
-            {
-              context: localIdentContext,
-              hashPrefix: localIdentHashPrefix,
-              regExp: localIdentRegExp,
-            }
-          );
+          let localIdent;
+
+          if (typeof getLocalIdent !== 'undefined') {
+            localIdent = getLocalIdent(
+              loaderContext,
+              localIdentName,
+              unescape(exportName),
+              {
+                context: localIdentContext,
+                hashPrefix: localIdentHashPrefix,
+                regExp: localIdentRegExp,
+              }
+            );
+          }
 
           // A null/undefined value signals that we should invoke the default
           // getLocalIdent method.
-          if (localIdent == null) {
+          if (typeof localIdent === 'undefined' || localIdent === null) {
             localIdent = defaultGetLocalIdent(
               loaderContext,
               localIdentName,
-              exportName,
+              unescape(exportName),
               {
                 context: localIdentContext,
                 hashPrefix: localIdentHashPrefix,
                 regExp: localIdentRegExp,
               }
             );
+
+            return escapeLocalident(localIdent).replace(
+              /\\\[local\\]/gi,
+              exportName
+            );
           }
-          return localIdent;
+
+          return escapeLocalident(localIdent);
         },
         exportGlobals: options.modules.exportGlobals,
       }),
diff --git a/test/__snapshots__/modules-option.test.js.snap b/test/__snapshots__/modules-option.test.js.snap
index da337d58..c50f1321 100644
--- a/test/__snapshots__/modules-option.test.js.snap
+++ b/test/__snapshots__/modules-option.test.js.snap
@@ -137,6 +137,58 @@ Array [
 
 exports[`"modules" option issue #861: warnings 1`] = `Array []`;
 
+exports[`"modules" option issue #966 - values in selectors aren't escaped properly: errors 1`] = `Array []`;
+
+exports[`"modules" option issue #966 - values in selectors aren't escaped properly: module 1`] = `
+"// Imports
+import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
+var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
+// Module
+___CSS_LOADER_EXPORT___.push([module.id, \\"._7-foo-class {\\\\n  color: red;\\\\n}\\\\n\\\\n.\\\\\\\\--bar-class {\\\\n  color: red;\\\\n}\\\\n\\\\n.\\\\\\\\--baz-class {\\\\n  color: red;\\\\n}\\\\n\\\\n.fooBaz-class-continuation {\\\\n  color: red;\\\\n}\\\\n\\\\n.some.class {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+// Exports
+___CSS_LOADER_EXPORT___.locals = {
+	\\"foo-class\\": \\"_7-foo-class\\",
+	\\"bar-class\\": \\"--bar-class\\",
+	\\"baz-class\\": \\"--baz-class\\",
+	\\"fooBaz-class\\": \\"fooBaz-class-continuation\\",
+	\\"some\\": \\"some\\",
+	\\"class\\": \\"class\\"
+};
+export default ___CSS_LOADER_EXPORT___;
+"
+`;
+
+exports[`"modules" option issue #966 - values in selectors aren't escaped properly: result 1`] = `
+Array [
+  Array [
+    "./modules/issue-966/issue-966.css",
+    "._7-foo-class {
+  color: red;
+}
+
+.\\\\--bar-class {
+  color: red;
+}
+
+.\\\\--baz-class {
+  color: red;
+}
+
+.fooBaz-class-continuation {
+  color: red;
+}
+
+.some.class {
+  color: red;
+}
+",
+    "",
+  ],
+]
+`;
+
+exports[`"modules" option issue #966 - values in selectors aren't escaped properly: warnings 1`] = `Array []`;
+
 exports[`"modules" option issue #966: errors 1`] = `Array []`;
 
 exports[`"modules" option issue #966: module 1`] = `
@@ -144,10 +196,10 @@ exports[`"modules" option issue #966: module 1`] = `
 import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
 var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(false);
 // Module
-___CSS_LOADER_EXPORT___.push([module.id, \\".button.hey {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
+___CSS_LOADER_EXPORT___.push([module.id, \\".button-hey {\\\\n  color: red;\\\\n}\\\\n\\", \\"\\"]);
 // Exports
 ___CSS_LOADER_EXPORT___.locals = {
-	\\"button\\": \\"button.hey\\"
+	\\"button\\": \\"button-hey\\"
 };
 export default ___CSS_LOADER_EXPORT___;
 "
@@ -157,7 +209,7 @@ exports[`"modules" option issue #966: result 1`] = `
 Array [
   Array [
     "./modules/issue-966/button.css",
-    ".button.hey {
+    ".button-hey {
   color: red;
 }
 ",
diff --git a/test/fixtures/modules/issue-966/issue-966.css b/test/fixtures/modules/issue-966/issue-966.css
new file mode 100644
index 00000000..a27ca561
--- /dev/null
+++ b/test/fixtures/modules/issue-966/issue-966.css
@@ -0,0 +1,19 @@
+.foo-class {
+  color: red;
+}
+
+.bar-class {
+  color: red;
+}
+
+.baz-class {
+  color: red;
+}
+
+.fooBaz-class {
+  color: red;
+}
+
+.some.class {
+  color: red;
+}
diff --git a/test/fixtures/modules/issue-966/issue-966.js b/test/fixtures/modules/issue-966/issue-966.js
new file mode 100644
index 00000000..99ef679a
--- /dev/null
+++ b/test/fixtures/modules/issue-966/issue-966.js
@@ -0,0 +1,5 @@
+import css from './issue-966.css';
+
+__export__ = css;
+
+export default css;
diff --git a/test/modules-option.test.js b/test/modules-option.test.js
index 0a7f4b02..06b7cd27 100644
--- a/test/modules-option.test.js
+++ b/test/modules-option.test.js
@@ -491,6 +491,45 @@ describe('"modules" option', () => {
     expect(getErrors(stats)).toMatchSnapshot('errors');
   });
 
+  it("issue #966 - values in selectors aren't escaped properly", async () => {
+    const compiler = getCompiler('./modules/issue-966/issue-966.js', {
+      modules: {
+        getLocalIdent: (loaderContext, localIdentName, localName) => {
+          if (localName === 'foo-class') {
+            return `7-${localName}`;
+          }
+
+          if (localName === 'bar-class') {
+            return `>-${localName}`;
+          }
+
+          if (localName === 'baz-class') {
+            return `\u0000-${localName}`;
+          }
+
+          if (localName === 'fooBaz-class') {
+            return `${localName}.continuation`;
+          }
+
+          return null;
+        },
+        localIdentName: '[local]',
+      },
+    });
+    const stats = await compile(compiler);
+
+    expect(
+      getModuleSource('./modules/issue-966/issue-966.css', stats)
+    ).toMatchSnapshot('module');
+
+    expect(getExecutedCode('main.bundle.js', compiler, stats)).toMatchSnapshot(
+      'result'
+    );
+
+    expect(getWarnings(stats)).toMatchSnapshot('warnings');
+    expect(getErrors(stats)).toMatchSnapshot('errors');
+  });
+
   it('issue #967', async () => {
     const compiler = getCompiler('./modules/issue-967/path-placeholder.js', {
       modules: {