Rock v18+ Only
This is an updated version of the recipe that takes advantage of features introduced in Rock v18. If you are running an earlier version of Rock, see the original Custom Icon Font recipe.

Description

Rock's use of Tabler Icons (formerly Font Awesome) is great. You have a huge library of icons at your disposal. Tabler usually has the exact icon you need, but sometimes the occasion calls for an extra special icon. This recipe will show you how to create and add custom icons to Rock that you can use just like Tabler icons.

In order to implement this recipe, you will need the ability to upload files directly to the file system of your Rock server, whether that's through FTP, Remote Desktop or some other means.

Credit

This recipe began life many years ago as a blog post by Mark Lee on a website called "Shoulder the Boulder". From my understanding, as the Rock community grew and the Recipe feature on the Rock community site was released, there was no longer as much of a need for Shoulder the Boulder, so the site was taken down. Ever since then, the only way to find old articles from the blog has been to search for them using the Wayback Machine on Archive.org.

Mark's article in particular has been such a huge help for me and many others in the community over the last few years that I felt it absolutely needed to be brought back and preserved in the form of a recipe. A majority of the content in this recipe was taken (with Mark's blessing) directly from his post with a few modifications and updated screenshots. So please, give Mark most of your thanks for this one!

If you're interested in adding custom icons to your check-in labels, be sure to check out Stephen Cracium's recipe titled Install CUSTOM Icon Font on Printer Label.

How-To

Create Some SVG Icons

The first thing you will need are icons. Your icons will need to be vector images (JPGs, GIFs, and PNGs won't work here.) You can create these with any vector program such as Adobe Illustrator (Paid) or Inkscape (Free). Inkscape is an open source tool that is perfect for creating vector icons. You will need to save your icons in the SVG format.

Generate Font Files

After you have your icons ready head over to icomoon.io. IcoMoon is a free online tool that you can use to turn your SVG files into a font that can be used in Rock.

  1. Start by clicking Old App button in the top left of the IcoMoon website. If you're familiar with the New App, you can use that, but this recipe will walk you through the old app since it's much simpler to use. I assume at some point the old app will go away and I will have to update this recipe, but for now the old app is a much better fit.
  2. IcoMoon will start you off with some free icons for your icon pack. For now ignore those icons and click the Import Icons button at the top left of the screen to start importing your icons. Browse to where you saved your SVG icons and select the ones you want to upload.
    'Import Icons' button
  3. Your icons will show up in IcoMoon, but they won't be included in your project until you 'select' them. Make sure the selection tool is active at the top and then click on the icons you want to include in your font.
    Select icons
  4. Optional: Change icon names and add search tags for ease of use later:

    Activate the edit tool at the top and then click on an icon to edit it.

    Activate 'Edit' tool

    In the edit box that pops up, add some search tags and make sure the icon name looks right. The search tags will help you find the icon later in Rock, so make sure to add every keyword you think someone might search for. The name will be part of the CSS class that will be used to dislay the icon on a web page. I recommend coming up with a consistent and logical naming convention for your icons so you know what you're looking at when editing your HTML in the future.

    Edit icon tags and name

    Repeat this step for all of the icons in your project.

  5. Once you're happy with your selections, click the Generate Font button at the bottom of the screen.
    'Generate Font' button
  6. Your icon font will be ready to download, but it isn't configured in a useful way. Click on the gear icon () next to the download button in the lower right to open the font preferences.
    'Font Preferences' button
  7. Set the following values:
    • Font Name: CustomIcons
    • Class Prefix: ci-
    • CSS Selector > Use a Class: .ci

    You can technically use anything you want for the Font Name, Class Prefix and CSS Selector values (as long as they're not the same ones that Tabler or Font Awesome use.) You'll just need to make adjustments accordingly in the remaining steps.

    I also like to fill in the Version and some of the Metadata fields. Version helps to be able to identify future updates you make to your icon font. Just make sure to increment the version number each time you make changes and generate a new font.

    Example font preference values

    Click the close icon () at the top right when you're done.

  8. Click the Download button to download a ZIP file of your new font files.

Optional: Download Your IcoMoon Project

IcoMoon only allows you to save your icon font project if you pay to become a premium member. Instead, you can download the details of your project as a JSON file and then import it again in the future when you need to make updates.

  1. Click the hamburger menu () at the top left and choose Manage Projects.
    IcoMoon Manage Projects menu item
  2. Rename the "Untitled Project" by clicking on the project name and replacing it with whatever you want.
    IcoMoon project management page
  3. Download your project using the Download link at the far right.
  4. Keep the downloaded JSON file in a safe place for the future.

Later, when you need to make changes to your icon font, just come back to IcoMoon and choose the Import Project option to upload your JSON project file. Once it's imported, just click on Load to pick up right where you left off.

Install The Font

The downloaded ZIP file will include the fonts that you will need, as well as the style.css file that will allow your icons to be displayed on a page.

  1. Extract the contents of the ZIP file to a folder on your computer.
  2. Connect to your Rock server and browse to the [Rock]/Assets/Fonts folder. Create a new sub-folder named CustomIcons. Upload the contents of the fonts folder from the ZIP file to the new folder.
    Assets/Fonts/CustomIcons folder

    If you already added a custom icon font using the previous version of my recipe, you can clean up a little by browsing to the [Rock]/Styles folder on the server and delete your old sub-folder named CustomIcons.

  3. Before updating your themes, you'll need to make a modification to style.css. Open the CSS file in your favorite text editor. Near the top you'll see three lines that look something like this:

    url('fonts/CustomIcons.ttf?6skikg') format('truetype'),
    url('fonts/CustomIcons.woff?6skikg') format('woff'),
    url('fonts/CustomIcons.svg?6skikg#CustomIcons') format('svg');

    Change all three file paths from fonts/CustomIcons.xyz?… to ../../../Assets/fonts/CustomIcons/CustomIcons.xyz?…

  4. Next you'll need to add the contents of style.css to each of your Themes in Rock. Log in to Rock and go to Admin Tools > CMS Configuration > Themes and edit one of your themes.
  5. Copy the contents of style.css and paste it into the theme's CSS Overrides box.
    Rock Theme CSS Overrides field

    If you previously added a custom icon font, you can remove the @import "../../../Styles/CustomIcons/style.less"; line from the CSS Overrides.

  6. Save your theme and repeat the previous two steps for each of your themes where you will need your custom icons.

    If you update your icon font in the future, just repeat all of the steps in this recipe again.

Additional Setup

Additional setup is needed for some of the latest features in Rock. Specifically, the new Obsidian icon picker control requires a special JSON file in order to include your icons in its search results. My Icon Picker recipe has also been rewritten to use the same JSON files.

Obsidian Icon Picker screenshot
  1. Create an "Icon Font Converter" Page
    Icon Font Converter screenshot

    Create a new child page under Admin Tools > Settings > CMS.

    • Page Title: Icon Font Converter
    • Site: Rock RMS
    • Layout: Full Width
    • Icon CSS Class: ti ti-file-import
    • Page Routes: admin/cms/icon-font-converter (or whatever makes sense)
  2. Add an HTML Content Block to the Main Zone

    Block Properties:

    No changes are needed for the block properties.

    HTML Content:

    Modify the first two lines if you named your font differently or used a different CSS class prefix for your icons. Nothing else needs to be modified.

    {%- assign outputName = 'custom-icons' %}
    {%- assign classPrefix = 'ci' %}
    //------------------------------
    
    //- icoMoon to Rock Icon Library JSON Converter
    <div class="panel panel-block">
        <div class="panel-heading">
            <h1 class="panel-title">
                <i class="fa fa-file-import"></i> icoMoon to Rock Icon Library Converter
            </h1>
        </div>
        <div class="panel-body">
            <div class="row">
                <div class="col-md-6">
    
                    <p>
                        Converts a <code>selection.json</code> export from icoMoon into the Rock RMS icon library format (<code>{{ outputName }}.json</code>).
                    </p>
    
                    <div id="icm-fileupload-group" class="form-group file-uploader fileupload-group-lg">
                        <div class="control-wrapper">
                            <div class="fileupload-group">
                                <div id="icm-thumbnail" class="fileupload-thumbnail">
                                    <span id="icm-filename" class="file-link"></span>
                                </div>
                                <div id="icm-drop-zone" class="fileupload-dropzone">
                                    <span>Drop File Here or Click to Select</span>
                                    <input type="file" id="icm-file-input" accept=".json,application/json">
                                </div>
                            </div>
                        </div>
                    </div>
    
                </div>
                <div class="col-md-6">
    
                    //- Alert area
                    <div id="icm-alert" style="display:none;"></div>
    
                    //- Results panel - hidden until conversion succeeds
                    <div id="icm-results" style="display:none;">
                        <div class="panel panel-success">
                            <div class="panel-heading">
                                <h3 class="panel-title">
                                    <i class="fa fa-check-circle"></i> Conversion Complete
                                </h3>
                            </div>
                            <div class="panel-body">
                                <ul id="icm-summary" class="list-unstyled"></ul>
                                <p>Download the converted JSON file and upload it to <code>/Assets/Icons/Libraries/</code></p>
                                <button id="icm-download-btn" class="btn btn-primary" type="button">
                                    <i class="fa fa-download"></i>&nbsp; Download {{ outputName }}.json
                                </button>
                            </div>
                        </div>
                    </div>
    
                    //- Warnings panel - shown only if icons were skipped
                    <div id="icm-warnings" style="display:none;">
                        <div class="panel panel-warning">
                            <div class="panel-heading">
                                <h3 class="panel-title">
                                    <i class="fa fa-exclamation-triangle"></i> Skipped Icons
                                </h3>
                            </div>
                            <ul id="icm-warnings-list" class="list-group"></ul>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        (function ()
        {
    
            var fileuploadGroup = document.getElementById('icm-fileupload-group');
            var dropZone = document.getElementById('icm-drop-zone');
            var fileInput = document.getElementById('icm-file-input');
            var thumbnail = document.getElementById('icm-thumbnail');
            var filenameEl = document.getElementById('icm-filename');
            var alertArea = document.getElementById('icm-alert');
            var resultsArea = document.getElementById('icm-results');
            var summaryEl = document.getElementById('icm-summary');
            var downloadBtn = document.getElementById('icm-download-btn');
            var warningsArea = document.getElementById('icm-warnings');
            var warningsList = document.getElementById('icm-warnings-list');
    
            var outputBlob = null;
    
            // Drag-and-drop
            fileuploadGroup.addEventListener('dragover', function (e)
            {
                e.preventDefault();
                fileuploadGroup.classList.add('icm-drag-over');
            });
    
            fileuploadGroup.addEventListener('dragleave', function (e)
            {
                // Only remove the class if leaving the group entirely, not a child element
                if (!fileuploadGroup.contains(e.relatedTarget))
                {
                    fileuploadGroup.classList.remove('icm-drag-over');
                }
            });
    
            fileuploadGroup.addEventListener('drop', function (e)
            {
                e.preventDefault();
    
                fileuploadGroup.classList.remove('icm-drag-over');
                var file = e.dataTransfer.files[0];
    
                if (file) processFile(file);
            });
    
            // File input change
            fileInput.addEventListener('change', function ()
            {
                if (fileInput.files[0]) processFile(fileInput.files[0]);
                fileInput.value = '';
            });
    
            // Download
            downloadBtn.addEventListener('click', function ()
            {
                if (!outputBlob) return;
    
                var url = URL.createObjectURL(outputBlob);
                var a = document.createElement('a');
    
                a.href = url;
                a.download = '{{ outputName }}.json';
    
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
    
                URL.revokeObjectURL(url);
            });
    
            // Core conversion
            function processFile(file)
            {
                outputBlob = null;
                hide(resultsArea);
                hide(warningsArea);
                hide(alertArea);
    
                if (!file.name.endsWith('.json'))
                {
                    showAlert('danger', '<strong>Invalid file type.</strong> Please select a <code>.json</code> file.');
                    resetUploader();
                    return;
                }
    
                // Show the selected filename in the thumbnail area
                filenameEl.textContent = file.name;
    
                showAlert('info', '<i class="fa fa-spinner fa-spin"></i> Reading file&hellip;');
    
                var reader = new FileReader();
    
                reader.onload = function (e)
                {
                    var data;
    
                    try
                    {
                        data = JSON.parse(e.target.result);
                    }
                    catch (err)
                    {
                        showAlert('danger', '<strong>Parse error.</strong> The file is not valid JSON.');
                        resetUploader();
                        return;
                    }
    
                    if (!data.icons || !Array.isArray(data.icons))
                    {
                        showAlert('danger', '<strong>Unrecognised format.</strong> This does not appear to be an icoMoon <code>selection.json</code> file. Expected a top-level <code>icons</code> array.');
                        resetUploader();
                        return;
                    }
    
                    var prefix = '{{ classPrefix }}';
                    var skipped = [];
                    var icons = [];
                    var defaultWidth = data.height || 1024;
    
                    data.icons.forEach(function (entry, idx)
                    {
                        var name = entry.properties && entry.properties.name;
                        var paths = entry.icon && entry.icon.paths;
    
                        if (!name || !paths || !Array.isArray(paths) || paths.length === 0)
                        {
                            skipped.push('Icon #' + (idx + 1) + ': missing name or path data');
                            return;
                        }
    
                        var rawTags = ((entry.icon && entry.icon.tags) || []).filter(function (t) { return t !== ''; });
                        var attrs = (entry.icon && entry.icon.attrs) || entry.attrs || [];
                        var width = (entry.icon && entry.icon.width) || defaultWidth;
                        var svg = buildSvg(paths, attrs, width);
    
                        // Split comma-separated names into individual aliases
                        var names = name.split(',').map(function (n) { return n.trim(); }).filter(Boolean);
    
                        names.forEach(function (alias)
                        {
                            // Always include the icon's own name as a search term
                            var searchTerms = rawTags.indexOf(alias) === -1
                                ? [alias].concat(rawTags)
                                : rawTags.slice();
    
                            icons.push(
                            {
                                Title: alias,
                                SearchTerms: searchTerms,
                                StyleClass: prefix + '-' + alias,
                                IconSvg: svg
                            });
                        });
                    });
    
                    var output = { StyleClassPrefix: prefix, Icons: icons };
                    var json = JSON.stringify(output, null, 2);
                    outputBlob = new Blob([json], { type: 'application/json' });
    
                    // Build summary
                    hide(alertArea);
                    summaryEl.innerHTML = '';
                    addSummaryItem('<i class="fa fa-check text-success"></i> <strong>' + icons.length + '</strong> icon' + (icons.length !== 1 ? 's' : '') + ' converted');
                    addSummaryItem('<i class="fa fa-check text-success"></i> <code>StyleClassPrefix</code>: <code>"' + prefix + '"</code>');
                    addSummaryItem('<i class="fa fa-check text-success"></i> Output filename: <code>{{ outputName }}.json</code>');
    
                    if (skipped.length)
                    {
                        addSummaryItem('<i class="fa fa-exclamation-triangle text-warning"></i> <strong>' + skipped.length + '</strong> icon' + (skipped.length !== 1 ? 's' : '') + ' skipped (see below)');
                    }
    
                    show(resultsArea);
    
                    // Build warnings list if needed
                    if (skipped.length)
                    {
                        warningsList.innerHTML = '';
                        skipped.forEach(function (msg)
                        {
                            var li = document.createElement('li');
                            li.className = 'list-group-item';
                            li.textContent = msg;
                            warningsList.appendChild(li);
                        });
                        show(warningsArea);
                    }
    
                    // Ready for another file
                    resetUploader();
                };
    
                reader.readAsText(file);
            }
    
            // SVG builder
            function buildSvg(paths, attrs, width)
            {
                var vw = width || 1024;
                var vh = 1024;
                var pathEls = paths.map(function (d, i)
                {
                    var attr = (attrs && attrs[i]) ? attrs[i] : {};
                    var fillAttr = ('fill' in attr) ? ' fill="' + attr.fill + '"' : '';
                    return '<path d="' + d + '"' + fillAttr + '/>';
                }).join('');
                return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + vw + ' ' + vh + '">' + pathEls + '</svg>';
            }
    
            // Helpers
            function resetUploader()
            {
                filenameEl.textContent = '';
                fileInput.value = '';
            }
    
            function showAlert(type, html)
            {
                alertArea.innerHTML = '<div class="alert alert-' + type + '">' + html + '</div>';
                alertArea.style.display = '';
            }
    
            function addSummaryItem(html)
            {
                var li = document.createElement('li');
                li.innerHTML = html;
                summaryEl.appendChild(li);
            }
    
            function show(el) { el.style.display = ''; }
            function hide(el) { el.style.display = 'none'; }
        }());
    </script>
    
    <style>
        #icm-fileupload-group { margin-bottom: 0; }
    
        #icm-fileupload-group .fileupload-dropzone { cursor: pointer; }
        #icm-fileupload-group .fileupload-dropzone input[type="file"] { cursor: pointer; }
    
        #icm-fileupload-group.icm-drag-over .fileupload-group { border-color: #ee7624; }
        #icm-fileupload-group.icm-drag-over .fileupload-dropzone { background-color: #fef9f555; }
    
        #icm-alert { margin-top: 2rem; }
    
        #icm-results,
        #icm-warnings { margin-top: 2rem; }
    
        #icm-summary { margin-bottom: 2rem; }
    </style>
Convert Your Icon Font

Use the converter to convert the selection.json file from your IcoMoon zip file to a custom-icons.json file that will work with Rock. Upload the custom-icons.json to [Rock]/Assets/Icons/Libraries.

Each time you make changes your icon font in the future, you'll also need to convert and upload a new version of custom-icons.json.

Add Your Icon Font Library

Find the Icon Libraries defined type and add a new defined value.

Value: CustomIcons Description: /Assets/Icons/Libraries/custom-icons.json

Go to Admin Tools > Settings > CMS > Obsidian Control Gallery, find the Icon Picker control and check to make sure your icons show up in the picker. At the moment, the Obsidian icon picker doesn't show up very many places in Rock yet, but at least you're set for the future now!

Start Using Your Icons!

Finally you can start using your custom icons. Instead of using the normal fa fa-[icon] or ti ti-[icon] formula, you will use ci ci-[icon]. Just replace [icon] with the name of whichever icon you want to use.

Unless you made changes on IcoMoon before downloading your font, the icon names should be the same as the original SVG file names but with dashes instead of spaces. You can see a full list of all of your icon names either on the IcoMoon Generate page or by opening the style.css file that you downloaded from IcoMoon.

Go crazy with your custom icons. Icons for your different ministries, icons for your campuses, icons just because you can. And if you don't feel like making your own icons, IcoMoon has plenty of free icons you can add to your project just as easily.

Custom icon examples

Bonus

As I mentioned above, the Obsidian Icon Picker isn't widely available everywhere in Rock yet, so in the meantime you might also want to check out my updated Icon Picker recipe that allows you to search all of your available icons from Tabler, Font Awesome and your new custom icon font from anywhere there's an "Icon CSS Class" field in Rock.

If you previously implemented my Icon Picker recipe already, you'll want to check it out again because it has been completely rewritten to work with Tabler icons and Rock's new Icon Libraries defined type.

Icon Picker screenshot

Follow Up

Please don't hesitate to leave a comment below or hit me up on Rock Chat (@JeffRichmond) if you have questions or find any issues with this recipe.

If you come up with better or more efficient ways of doing anything in this recipe, please let me know. Thanks!


Change Log

  • 2022-11-11 - Initial Version
  • 2024-04-23 - Added a link to Stephen's check-in label recipe
  • 2026-06-19 - Overhauled for Rock v18+