Easy Theme Development with Inheritance

How to develop themes based on a parent theme...

4 mins

Most people that are initially developing with Grav either modify the default Antimatter theme directly, or copy-and-rename it, providing a separate theme to modify. Each of these approaches have their own issues.

By modifying the base theme directly, any theme update will potentially overwrite changes made. By copying the theme, the theme will not be overridden, but updates and fixes to the core theme, have to be manually merged over to the copied theme.

There is a much simpler, and much more maintainable method however: Theme Inheritance.

Power of Streams

Grav makes extensive use of PHP Streams throughout it's architecture to allow locations of assets and resources to be customizable, and also to provide powerful override capabilities. The most obvious usage of these streams are utilized can be observed in our configuration system. You can override system options, with user provided YAML files, and you can override those with environment-based configuration files.

Streams are also used in themes to tell Grav where to look for Twig templates as well as CSS and JavaScript assets. Implementing the streams is as simple as adding a few lines to a YAML file.

Create Your New Theme

Let's walk through a simple example.

  1. Create a new folder: user/themes/mytheme to house your new theme.
  2. Create a new theme YAML file: /user/themes/mytheme/mytheme.yaml with the following content:
    streams:
     schemes:
       theme:
         type: ReadOnlyStream
         prefixes:
           '':
             - user/themes/mytheme
             - user/themes/antimatter
  3. Change your default theme to use your new mytheme by editing the pages: theme: option in your user/config/system.yaml configuration file:
    pages:
     theme: mytheme

You have now created a new theme called mytheme and set up the streams so that it will first look in the mytheme theme first, then try antimatter. So in essence, Antimatter is the base-theme for this new theme.

Make a CSS Modification

The most common modification type involves adding custom CSS styling. The default antimatter theme already supports the ability to use a css/custom.css file, so all that is needed to be done is to provide one:

mytheme
├── css
│   └── custom.css
└── mytheme.yaml

In this theme we'll put a quick override to ensure things are working as expected:

body a { color: #f00; }

If you reload your browser pointing at your Grav installation, you should see the links all in an awfully-bright hue of red:

If you view the source you will see the proof is in the pudding!

<link href="/user/themes/antimatter/css-compiled/nucleus.css" type="text/css" rel="stylesheet" />
<link href="/user/themes/antimatter/css-compiled/template.css" type="text/css" rel="stylesheet" />
<link href="/user/themes/mytheme/css/custom.css" type="text/css" rel="stylesheet" />
<link href="/user/themes/antimatter/css/font-awesome.min.css" type="text/css" rel="stylesheet" />
<link href="/user/themes/antimatter/css/slidebars.min.css" type="text/css" rel="stylesheet" />

See how all the regular CSS files come from the antimatter theme, while the new custom.css is being loaded automatically from mytheme.

Make a Template Modification

The purpose of creating your own theme is so that you can make modifications as you need them. For example, perhaps you want to modify the main navigation to add a couple of static links to the end of it. All you need to do is provide an updated version of the templates/partials/navigation.html.twig file. So create the 'templates/partials` folder structure, and copy over the original template from antimatter:

mytheme
├── mytheme.yaml
└── templates
    └── partials
        └── navigation.html.twig

Then edit the navigation.html.twig file and add your customizations. In this case we are simply going to add a link to the getgrav.org site at the end of our normal links:

...
<ul class="navigation">
    {% if config.themes.antimatter.dropdown.enabled %}
        {{ _self.loop(pages) }}
    {% else %}
        {% for page in pages.children %}
            {% if page.visible %}
                {% set current_page = (page.active or page.activeChild) ? 'active' : '' %}
                <li class="{{ current_page }}">
                    <a href="{{ page.url }}">
                        {% if page.header.icon %}<i class="fa fa-{{ page.header.icon }}"></i>{% endif %}
                        {{ page.menu }}
                    </a>
                </li>
            {% endif %}
        {% endfor %}
    {% endif %}
    <li><a href="http://getgrav.org">GetGrav.org</a></li>
</ul>

Save the file and reload the page. You should see your changes in action:

Advanced Customization

If you want to add a new JavaScript feature for example, you will probably need to add the .js file to your theme, and also copy-over and modify the templates/partials/base.html.twig file so you can reference it with the other JavaScript files.

If you wish to make more extensive CSS customizations that go beyond a simple custom.css file, you may wish to copy over the entire scss/ folder and make your changes there, compiling the resulting CSS into a local css-compiled/ folder.

You can of course customize any part of the original base-theme, just be aware the more you customize, and the more you override, the more that your custom theme will diverge from the base theme that it is inheriting.

If there comes a point where you really don't want to rely on the base theme, you can just copy over the missing elements from Antimatter and remove the stream from the YAML.