diff --git a/Gruntfile.js b/Gruntfile.js
index 467443bbf3df1af2a63882ced1100d67de693246..01280f1026a1853a70052770c5ef995a79f2799a 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -14,6 +14,17 @@ module.exports = function(grunt) {
 
     // Iterate through theme.config.php files collecting parent themes in search path:
     while (config = fs.readFileSync("themes/" + parts[1] + "/theme.config.php", "UTF-8")) {
+      // First identify mixins:
+      var mixinMatches = config.match(/["']mixins["']\s*=>\s*\[([^\]]+)\]/);
+      if (mixinMatches !== null) {
+        var mixinParts = mixinMatches[1].split(',')
+        for (var i = 0; i < mixinParts.length; i++) {
+          parts[1] = mixinParts[i].trim().replace(/['"]/g, '');
+          retVal.push(parts.join('/') + '/');
+        }
+      }
+
+      // Now move up to parent theme:
       var matches = config.match(/["']extends["']\s*=>\s*['"](\w+)['"]/);
 
       // "extends" set to "false" or missing entirely? We've hit the end of the line:
diff --git a/module/VuFindConsole/config/module.config.php b/module/VuFindConsole/config/module.config.php
index 3d8e83c5e2f453047a1cc22a97ad9f8ff9f9f4b9..086187c098d03cdc6336a90403b6cd4bc6174374 100644
--- a/module/VuFindConsole/config/module.config.php
+++ b/module/VuFindConsole/config/module.config.php
@@ -43,6 +43,7 @@ $routes = [
     'generate/recordroute' => 'generate recordroute [<base>] [<newController>] [<module>]',
     'generate/staticroute' => 'generate staticroute [<name>] [<module>]',
     'generate/theme' => 'generate theme [<themename>]',
+    'generate/thememixin' => 'generate thememixin [<name>]',
     // harvest/harvest_oai is too complex to represent here; we need to rely on default-route
     'harvest/merge-marc' => 'harvest merge-marc [<dir>]',
     'import/import-xsl' => 'import import-xsl [--test-only] [--index=] [<xml>] [<properties>]',
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index 86b0709357315264e5e030349a37df4f63aceecd..db7ecc6fa2d3e3be580c9c69813d55dc75df8595 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -320,6 +320,33 @@ class GenerateController extends AbstractBase
         return $this->getSuccessResponse();
     }
 
+    /**
+     * Create a custom theme from the template.
+     *
+     * @return \Zend\Console\Response
+     */
+    public function thememixinAction()
+    {
+        // Validate command line argument:
+        $request = $this->getRequest();
+        $name = $request->getParam('name');
+        if (empty($name)) {
+            Console::writeLine("\tNo mixin name found, using \"custom\"");
+            $name = 'custom';
+        }
+
+        // Use the theme generator to create and configure the theme:
+        $generator = $this->serviceLocator->get('VuFindTheme\MixinGenerator');
+        if (!$generator->generate($name)) {
+            Console::writeLine($generator->getLastError());
+            return $this->getFailureResponse();
+        }
+        Console::writeLine(
+            "\tFinished. Add to your theme.config.php 'mixins' setting to activate."
+        );
+        return $this->getSuccessResponse();
+    }
+
     /**
      * Create a new subclass and factory to override a factory-generated
      * service.
diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php
index eac58949210c314c42891353181aa3d51411a074..5af6790aace8b4631e0c0ddecf7fe31aea07629a 100644
--- a/module/VuFindTheme/Module.php
+++ b/module/VuFindTheme/Module.php
@@ -64,6 +64,8 @@ class Module
     {
         return [
             'factories' => [
+                'VuFindTheme\MixinGenerator' =>
+                    'VuFindTheme\Module::getMixinGenerator',
                 'VuFindTheme\ThemeCompiler' =>
                     'VuFindTheme\Module::getThemeCompiler',
                 'VuFindTheme\ThemeGenerator' =>
@@ -98,6 +100,18 @@ class Module
         ];
     }
 
+    /**
+     * Factory function for MixinGenerator object.
+     *
+     * @param ServiceManager $sm Service manager
+     *
+     * @return MixinGenerator
+     */
+    public static function getMixinGenerator(ServiceManager $sm)
+    {
+        return new MixinGenerator($sm->get('VuFindTheme\ThemeInfo'));
+    }
+
     /**
      * Factory function for ThemeCompiler object.
      *
diff --git a/module/VuFindTheme/src/VuFindTheme/MixinGenerator.php b/module/VuFindTheme/src/VuFindTheme/MixinGenerator.php
new file mode 100644
index 0000000000000000000000000000000000000000..957820399658542c0a0c51388bd6bcc886521f15
--- /dev/null
+++ b/module/VuFindTheme/src/VuFindTheme/MixinGenerator.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Class to generate a new mixin from a template.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2017.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Theme
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFindTheme;
+use Zend\Console\Console;
+
+/**
+ * Class to generate a new mixin from a template.
+ *
+ * @category VuFind
+ * @package  Theme
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class MixinGenerator extends AbstractThemeUtility
+{
+    /**
+     * Generate a new mixin from a template.
+     *
+     * @param string $name     Name of mixin to generate.
+     * @param string $template Name of template mixin directory
+     *
+     * @return bool
+     */
+    public function generate($name, $template = 'local_mixin_example')
+    {
+        // Check for existing theme
+        $baseDir = $this->info->getBaseDir() . '/';
+        if (realpath($baseDir . $name)) {
+            return $this->setLastError('Mixin "' . $name . '" already exists');
+        }
+        Console::writeLine('Creating new mixin: "' . $name . '"');
+        $source = $baseDir . $template;
+        $dest = $baseDir . $name;
+        Console::writeLine("\tCopying $template");
+        Console::writeLine("\t\tFrom: " . $source);
+        Console::writeLine("\t\tTo: " . $dest);
+        return $this->copyDir($source, $dest);
+    }
+}
diff --git a/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php b/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
index 89d3d2b497a01f717fd31e643b7b7848aa38fef5..7070d65a2cecff8211800b0e4dc3d1a26e53b542 100644
--- a/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
+++ b/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
@@ -124,6 +124,9 @@ class ThemeCompiler extends AbstractThemeUtility
                 $dest[$key] = $this
                     ->mergeConfig($value, isset($dest[$key]) ? $dest[$key] : []);
                 break;
+            case 'mixins':
+                // Omit mixin settings entirely
+                break;
             default:
                 // Default behavior: merge arrays, let existing flat settings
                 // trump new incoming ones:
diff --git a/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php b/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
index 43e0731c11643c2c683ce64cd67ef2af1dcac214..70f76d5141490e44bfa420fba89677fbbbb641a2 100644
--- a/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
+++ b/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
@@ -92,6 +92,18 @@ class ThemeInfo
         return $this->baseDir;
     }
 
+    /**
+     * Get the configuration file for the specified mixin.
+     *
+     * @param string $mixin Mixin name
+     *
+     * @return string
+     */
+    protected function getMixinConfig($mixin)
+    {
+        return $this->baseDir . "/$mixin/mixin.config.php";
+    }
+
     /**
      * Get the configuration file for the specified theme.
      *
@@ -136,6 +148,26 @@ class ThemeInfo
         return $this->currentTheme;
     }
 
+    /**
+     * Load configuration for the specified theme (and its mixins, if any) into the
+     * allThemeInfo property.
+     *
+     * @param string $theme Name of theme to load
+     *
+     * @return void
+     */
+    protected function loadThemeConfig($theme)
+    {
+        // Load theme configuration...
+        $this->allThemeInfo[$theme] = include $this->getThemeConfig($theme);
+        // ..and if there are mixins, load those too!
+        if (isset($this->allThemeInfo[$theme]['mixins'])) {
+            foreach ($this->allThemeInfo[$theme]['mixins'] as $mix) {
+                $this->allThemeInfo[$mix] = include $this->getMixinConfig($mix);
+            }
+        }
+    }
+
     /**
      * Get all the configuration details related to the current theme.
      *
@@ -149,8 +181,7 @@ class ThemeInfo
             $this->allThemeInfo = [];
             $currentTheme = $this->getTheme();
             do {
-                $this->allThemeInfo[$currentTheme]
-                    = include $this->getThemeConfig($currentTheme);
+                $this->loadThemeConfig($currentTheme);
                 $currentTheme = $this->allThemeInfo[$currentTheme]['extends'];
             } while ($currentTheme);
         }
@@ -180,16 +211,21 @@ class ThemeInfo
         $allThemeInfo = $this->getThemeInfo();
 
         while (!empty($currentTheme)) {
-            foreach ($allPaths as $currentPath) {
-                $file = "$basePath/$currentTheme/$currentPath";
-                if (file_exists($file)) {
-                    if (true === $returnType) {
-                        return $file;
-                    } else if (self::RETURN_ALL_DETAILS === $returnType) {
-                        return ['path' => $file, 'theme' => $currentTheme];
+            $currentThemeSet = array_merge(
+                (array) $currentTheme,
+                isset($allThemeInfo[$currentTheme]['mixins'])
+                    ? $allThemeInfo[$currentTheme]['mixins'] : []
+            );
+            foreach ($currentThemeSet as $theme) {
+                foreach ($allPaths as $currentPath) {
+                    $path = "$basePath/$theme/$currentPath";
+                    if (file_exists($path)) {
+                        // Depending on return type, send back the requested data:
+                        if (self::RETURN_ALL_DETAILS === $returnType) {
+                            return compact('path', 'theme');
+                        }
+                        return $returnType ? $path : $theme;
                     }
-                    // Default return type:
-                    return $currentTheme;
                 }
             }
             $currentTheme = $allThemeInfo[$currentTheme]['extends'];
diff --git a/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/hello.js b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/hello.js
new file mode 100644
index 0000000000000000000000000000000000000000..d91d6f72456f9d12f9b9a7d8d2e9c39f8e24f8e7
--- /dev/null
+++ b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/hello.js
@@ -0,0 +1 @@
+alert('hello, mixin');
\ No newline at end of file
diff --git a/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/mixin.js b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/mixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..1efc4f412e973d7b1ce0d375b5a2251a0e4d66d5
--- /dev/null
+++ b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/js/mixin.js
@@ -0,0 +1 @@
+// no code in demo file
\ No newline at end of file
diff --git a/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/mixin.config.php b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/mixin.config.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd8e12f22a446523339f864a30a92e0a6324c25e
--- /dev/null
+++ b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin/mixin.config.php
@@ -0,0 +1,4 @@
+<?php
+return [
+    'js' => ['mixin.js'],
+];
diff --git a/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin_user/theme.config.php b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin_user/theme.config.php
new file mode 100644
index 0000000000000000000000000000000000000000..93c5a8d527a33fd4fa18f5721d401e77c9f4a537
--- /dev/null
+++ b/module/VuFindTheme/tests/unit-tests/fixtures/themes/mixin_user/theme.config.php
@@ -0,0 +1,5 @@
+<?php
+return [
+    'extends' => 'child',
+    'mixins' => ['mixin'],
+];
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeCompilerTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeCompilerTest.php
index 6640bd8271afc3b612d1764d0e9840d442cfb006..dcaedb3b18b572f8c25dc4786fe8db03575cf873 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeCompilerTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeCompilerTest.php
@@ -136,6 +136,64 @@ class ThemeCompilerTest extends Unit\TestCase
         $this->assertEquals($expectedConfig, $mergedConfig);
     }
 
+    /**
+     * Test the compiler with a mixin.
+     *
+     * @return void
+     */
+    public function testStandardCompilationWithMixin()
+    {
+        $baseDir = $this->info->getBaseDir();
+        $parentDir = $baseDir . '/parent';
+        $childDir = $baseDir . '/child';
+        $mixinDir = $baseDir . '/mixin';
+        $compiler = $this->getThemeCompiler();
+        $result = $compiler->compile('mixin_user', 'compiled');
+
+        // Did the compiler report success?
+        $this->assertEquals('', $compiler->getLastError());
+        $this->assertTrue($result);
+
+        // Was the target directory created with the expected files?
+        $this->assertTrue(is_dir($this->targetPath));
+        $this->assertTrue(file_exists("{$this->targetPath}/parent.txt"));
+        $this->assertTrue(file_exists("{$this->targetPath}/child.txt"));
+        $this->assertTrue(file_exists("{$this->targetPath}/js/mixin.js"));
+
+        // Did the right version of the  file that exists in both parent and child
+        // get copied over?
+        $this->assertEquals(
+            file_get_contents("$mixinDir/js/hello.js"),
+            file_get_contents("{$this->targetPath}/js/hello.js")
+        );
+        $this->assertNotEquals(
+            file_get_contents("$childDir/js/hello.js"),
+            file_get_contents("{$this->targetPath}/js/hello.js")
+        );
+        $this->assertNotEquals(
+            file_get_contents("$parentDir/js/hello.js"),
+            file_get_contents("{$this->targetPath}/js/hello.js")
+        );
+
+        // Did the configuration merge correctly?
+        $expectedConfig = [
+            'extends' => false,
+            'css' => ['child.css'],
+            'js' => ['hello.js', 'extra.js', 'mixin.js'],
+            'helpers' => [
+                'factories' => [
+                    'foo' => 'fooOverrideFactory',
+                    'bar' => 'barFactory',
+                ],
+                'invokables' => [
+                    'xyzzy' => 'Xyzzy',
+                ]
+            ],
+        ];
+        $mergedConfig = include "{$this->targetPath}/theme.config.php";
+        $this->assertEquals($expectedConfig, $mergedConfig);
+    }
+
     /**
      * Test overwrite protection.
      *
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
index 35c17dff5bdb95393dc0bb524030968c0b3a4583..0cf095e7eea96691f7e14ef637de353d4dc9d821 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
@@ -109,6 +109,33 @@ class ThemeInfoTest extends Unit\TestCase
         );
     }
 
+    /**
+     * Test theme info with a mixin
+     *
+     * @return void
+     */
+    public function testGetThemeInfoWithMixin()
+    {
+        $ti = $this->getThemeInfo();
+        $ti->setTheme('mixin_user');
+        $expectedChild = include "{$this->fixturePath}/child/theme.config.php";
+        $expectedParent = include "{$this->fixturePath}/parent/theme.config.php";
+        $expectedMixin = include "{$this->fixturePath}/mixin/mixin.config.php";
+        $expectedMixinUser
+            = include "{$this->fixturePath}/mixin_user/theme.config.php";
+        $this->assertEquals('parent', $expectedChild['extends']);
+        $this->assertEquals(false, $expectedParent['extends']);
+        $this->assertEquals(
+            [
+                'mixin' => $expectedMixin,
+                'mixin_user' => $expectedMixinUser,
+                'child' => $expectedChild,
+                'parent' => $expectedParent
+            ],
+            $ti->getThemeInfo()
+        );
+    }
+
     /**
      * Test unfindable item.
      *
@@ -135,6 +162,19 @@ class ThemeInfoTest extends Unit\TestCase
         $this->assertEquals($expected, $ti->findContainingTheme('parent.txt', ThemeInfo::RETURN_ALL_DETAILS));
     }
 
+    /**
+     * Test findContainingTheme() with a mixin
+     *
+     * @return void
+     */
+    public function testFindContainingThemeWithMixin()
+    {
+        $ti = $this->getThemeInfo();
+        $ti->setTheme('mixin_user');
+        $this->assertEquals('mixin', $ti->findContainingTheme('js/mixin.js'));
+        $this->assertEquals('child', $ti->findContainingTheme('child.txt'));
+    }
+
     /**
      * Get a test object
      *
diff --git a/themes/local_mixin_example/js/mixin-popup.js b/themes/local_mixin_example/js/mixin-popup.js
new file mode 100644
index 0000000000000000000000000000000000000000..4006f884be462cb8d3306021492b0152af5e9c51
--- /dev/null
+++ b/themes/local_mixin_example/js/mixin-popup.js
@@ -0,0 +1 @@
+alert('Your mixin is loaded; please customize it to remove this annoying example script.');
\ No newline at end of file
diff --git a/themes/local_mixin_example/mixin.config.php b/themes/local_mixin_example/mixin.config.php
new file mode 100644
index 0000000000000000000000000000000000000000..b7b10fe6ebc0cd025ff2a31e9956f49c304e8933
--- /dev/null
+++ b/themes/local_mixin_example/mixin.config.php
@@ -0,0 +1,4 @@
+<?php
+return [
+    'js' => ['mixin-popup.js'],
+];
\ No newline at end of file