Skip to content

Theme Reference

Introduction

Since 2025, the recommended approach for theming CiviCRM is using the River theme system.

This page details the lower-level mechanics of theme engine in CiviCRM, and how to create a theme outside of the River system.

Defining a new CiviCRM Theme in an extension

To define a new theme, create an extension, e.g.

civix generate:module org.civicrm.newyork

and generate a skeletal theme file:

cd org.civicrm.newyork
civix generate:theme

Multiple subthemes

If you prefer to put multiple subthemes in the same extension, then you can pass an extra parameter. For example, this would generate themes named astoria and wallstreet:

civix generate:theme astoria
civix generate:theme wallstreet

The generate:theme command creates a theme definition (eg newyork.theme.php) which will be returned via hook_civicrm_themes. You might edit this file to include a nicer title.

// FILE: newyork.theme.php
array(
  'name' => 'newyork',
  'title' => 'New York',
  ...
)

Additionally, it creates placeholder copies of civicrm.css and bootstrap.css (which you can use or edit or replace per taste).

Now activate the theme, e.g.

cv en newyork
cv api setting.create theme_frontend=newyork theme_backend=newyork

Whenever a CiviCRM screen adds a CSS file via addStyleFile(), it will perform a search for the file -- first, looking in the active theme; then, looking for a fallback in civicrm-core. A typical directory tree would look like this:

org.civicrm.newyork/info.xml
org.civicrm.newyork/newyork.php
org.civicrm.newyork/css/civicrm.css
org.civicrm.newyork/css/bootstrap.css

Vocabularies

A vocabulary provides a list of CSS class-names. It defines a contract between an application-developer (who generates HTML data) and a theme-developer (who generates CSS data). The best way to learn about common vocabularies is to install the org.civicrm.styleguide extension.

There are two important vocabularies which correlate to two important files:

  • crm-* (aka civicrm.css) defines the look-and-feel for any screens based on the crm-* coding convention.
    • Strength: This is the traditional vocabulary used on the most screens. It has some smart conventions for identifying/selecting fields.
    • Weakness: Developed organically. It has gone through some iterations of cleanup/improvement, but its definition and terms are not strongly documented.
  • BootstrapCSS (aka bootstrap.css) defines the look-and-feel for any screens based on the Bootstrap coding convention.
    • Strength: Widely used vocabulary with a larger ecosystem and support tools (e.g. BootstrapSCSS).
    • Weakness: This is newer in the CiviCRM ecosystem. Not included with civicrm-core -- and there's no Greenwich for BootstrapCSS.

The basic purpose of a theme is to provide a copy of each CSS file.

Additional vocabularies

If you need to define a new vocabulary for widgets and concepts that don't exist in crm-* (civicrm.css) or BootstrapCSS (bootstrap.css), then you can simply choose another filename (superwidgets.css) and implement a style-guide. The theme system will allow themes to override any CSS file.

However, this doesn't mean that every CSS file should be overriden. From the perspective of an application developer, adhoc CSS often isn't intended for consumption/override by others. From the perspective of a theme developer, it would be overwhelming to override every CSS file from every extension.

Instead, approach new vocabularies conscientiously -- a new vocabulary should represent a contract in which both application developers and theme developers benefit from a clearer specification. Use a style-guide to document the contract.

Mechanics

Suppose we have a theme -- such as Greenwich or Shoreditch -- which defines a file -- such as civicrm.css. How does this file get loaded on to the screen?

Somewhere within the application, there is a call to Resources::addStyleFile(), as in:

Civi::resources()->addStyleFile('civicrm', 'css/civicrm.css');

The active theme gets first-priority at picking the actual content of css/civicrm.css. If it doesn't provide one, then it falls back to whatever default would normally be loaded.

Internally, addStyleFile() accesses the theming service (Civi::service('themes')). The theme service identifies the active-theme and then asks for the concrete URL for the CSS file.

You can simulate this through the command line with cv.

$ cv ev 'return Civi::service("themes")->getActiveThemeKey();'
"greenwich"
$ cv ev 'return Civi::service("themes")->resolveUrls("greenwich", "civicrm", "css/civicrm.css");'
[
    "http://dmaster.l/sites/all/modules/civicrm/css/civicrm.css?r=gWD8J"
]

Each theme has some configuration and metadata. You can inspect the metadata using getAll().

$ cv ev 'return Civi::service("themes")->getAll();'
{
    ...
    "greenwich": {
        "name": "greenwich",
        "url_callback": "\\Civi\\Core\\Themes\\Resolvers::simple",
        "search_order": [
            "greenwich",
            "_fallback_"
        ],
        "ext": "civicrm",
        "title": "Greenwich",
        "help": "CiviCRM 4.x look-and-feel"
    }
    ...
}

Internally, getAll() emits hook_civicrm_themes. This allows third-party packages to register themes. The metadata is cached, but you can clear that cache with a general system flush (cv flush).

Theme Metadata

The metadata for the theme allows several more fields. These fields are most useful if you intend to bundle multiple subthemes into the same package.

excludes

CiviCRM theming supports fallbacks: if you don't define civicrm.css in your theme, then it will fallback to using a version that is bundled in civicrm-core. But what if you want to exclude the file completely? For example, if you have provided styling rules through a CMS theme, then loading civicrm.css could be redundant. Use the excludes option to disable a file:

// FILE: newyork.php
function newyork_civicrm_themes(&$themes) {
  $themes['newyork'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'New York',
    'excludes' => array('css/civicrm.css'),
  );
}

prefix

If you have several variations on a theme, you may wish to define all of them in one extension. For example, the newyork extension might define themes for astoria and wallstreet. You can load each variant from a subfolder:

// FILE: newyork.php
function newyork_civicrm_themes(&$themes) {
  $themes['astoria'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'Astoria',
    'prefix' => 'astoria/',
  );
  $themes['wallstreet'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'Wall Street',
    'prefix' => 'wallstreet/',
  );
}

The corresponding file structure would be:

org.civicrm.newyork/info.xml
org.civicrm.newyork/newyork.php
org.civicrm.newyork/astoria/css/civicrm.css
org.civicrm.newyork/astoria/css/bootstrap.css
org.civicrm.newyork/wallstreet/css/civicrm.css
org.civicrm.newyork/wallstreet/css/bootstrap.css

search_order

Sometimes you may want to share files among themes; for example, the astoria and wallstreet themes might use a common version of civicrm.css (but have their own versions of bootstrap.css). You may manipulate the search_order to define your own fallback sequence:

// FILE: newyork.php
function newyork_civicrm_themes(&$themes) {
  $themes['astoria'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'Astoria',
    'prefix' => 'astoria/',
    'search_order' => array('astoria', '_newyork_common_', '_fallback_'),
  );
  $themes['wallstreet'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'Wall Street',
    'prefix' => 'wallstreet/',
    'search_order' => array('wallstreet', '_newyork_common_', '_fallback_'),
  );
  $themes['_newyork_common_'] = array(
    'ext' => 'org.civicrm.newyork',
    'title' => 'New York (Base Theme)',
    'prefix' => 'common/',
  );
  // Note: "_newyork_common_" begins with "_".  It is a hidden, abstract
  // theme which cannot be directly activated.
}

The corresponding file structure would be:

org.civicrm.newyork/info.xml
org.civicrm.newyork/newyork.php
org.civicrm.newyork/common/css/civicrm.css
org.civicrm.newyork/astoria/css/bootstrap.css
org.civicrm.newyork/wallstreet/css/bootstrap.css

url_callback

The previous theming examples are based on file-name conventions. However, file-name conventions are fairly static and may be unsuitable in cases like:

  • Dynamically generated themes defined through an admin GUI
  • Large theme libraries with complex rules for sharing/compiling CSS files
  • Integration with other theming systems that use different file-names

In all these cases, it may be useful to define a url_callback which provides more dynamic, fine-grained control over CSS loading.

// FILE: newyork.php
function newyork_civicrm_themes(&$themes) {
  foreach (array('blue', 'white', 'hicontrast') as $colorScheme) {
    $themes["newyork-{$colorScheme}"] = array(
      'ext' => "org.civicrm.newyork",
      'title' => "New York ({$colorScheme})",
      'url_callback' => "_newyork_css_url",
    );
  }
}

/**
 * Determine the URL for a CSS resource file.
 *
 * @param \Civi\Core\Themes $themes
 * @param string $themeKey
 *   Identify the active theme (ex: 'newyork-blue', 'newyork-hicontrast').
 * @param string $cssExt
 *   Identify the requested CSS file (ex: 'civicrm', 'org.civicrm.volunteer').
 * @param string $cssFile
 *   Identify the requested CSS file (ex: 'css/civicrm.css', 'css/bootstrap.css').
 * @return array|\Civi\Core\Themes::PASSTHRU
 *   A list of zero or more CSS URLs.
 *   To pass responsibility to another URL callback, return
 *   the constant \Civi\Core\Themes::PASSTHRU.
 */
function _newyork_css_url($themes, $themeKey, $cssExt, $cssFile) {
  return array('http://example.com/css/myfile.css');
}

The logic in _newyork_css_url() is fairly open-ended. A few tricks that may be useful:

  • Wrap other themes using $themes->resolveUrl(...)
  • Wrap other callbacks like \Civi\Core\Themes\Resolvers::simple(...) or \Civi\Core\Themes\Resolvers::fallback(...)
  • Locate files in an extension using Civi::resources()->getPath(...) or Civi::resources()->getUrl(...)
  • Generate files in a datadir using Civi::paths()->getPath(...) or Civi::paths()->getUrl(...)

In this example, the newyork theme supplements the civicrm.css file (adding its own content afterward) instead of overriding. All other CSS files work as normal overrides.

function _newyork_css_url($themes, $themeKey, $cssExt, $cssFile) {
  $urls = \Civi\Core\Themes\Resolvers::simple($themes, $themeKey, $cssExt, $cssFile);
  switch ("{$cssExt}/{$cssFile}") {
    case 'civicrm/css/civicrm.css':
      $urls = array_merge(
        Civi::service('themes')->resolveUrls('greenwich', $cssExt, $cssFile),
        $urls
      );
  }
  return $urls;
}

In our most sophisticated example, the newyork theme generates the civicrm.css content dynamically - by combining various CSS files and evaluating some inline variables ({{NEWYORK_URL}}). This uses the asset builder for caching.

function _newyork_civicrm_css_url($themes, $themeKey, $cssExt, $cssFile) {
  switch ("{$cssExt}/{$cssFile}") {
    case 'civicrm/css/civicrm.css':
      return [\Civi::service("asset_builder")->getUrl("newyork-civicrm.css", ['themeKey' => $themeKey])];
    default:
      return \Civi\Core\Themes\Resolvers::simple($themes, $themeKey, $cssExt, $cssFile);

  }
}

function newyork_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
  if ($asset !== 'newyork-civicrm.css') return;

  $rawCss = file_get_contents(Civi::resources()->getPath('civicrm', 'css/civicrm.css'))
    . "\n" . file_get_contents(E::path('newyork-part-1.css'))
    . "\n" . file_get_contents(E::path('newyork-part-2.css'));

  $vars = [
    '{{CIVICRM_URL}}'=> Civi::paths()->getUrl('[civicrm.root]/.'),
    '{{NEWYORK_URL}}' => E::url(),
  ];
  $mimeType = 'text/css';
  $content = strtr($rawCss, $vars);
}

Extension CSS files

Generally, one should only override the civicrm.css and bootstrap.css files. If some styling issue cannot be addressed well through those files, then you should probably have some discussion about how to improve the coding-conventions or the style-guide so that the standard CSS is good enough.

However, there may be edge-cases where you wish to override other CSS files. The file structure should match the original file structure. If you wish to override a CSS file defined by another extension, then include the extension as part of the name.

Original FileTheme File
civicrm-core/css/dashboard.cssorg.civicrm.newyork/css/dashboard.css
civicrm-core/ang/crmMailing.cssorg.civicrm.newyork/ang/crmMailing.css
org.civicrm.volunteer/css/main.cssorg.civicrm.newyork/org.civicrm.volunteer-css/dashboard.css
org.civicrm.rules/style/admin.cssorg.civicrm.newyork/org.civicrm.rules-style/admin.css

If you use a multitheme/prefixed configuration, then theme prefixes apply accordingly.

Original FileTheme File
civicrm-core/css/dashboard.cssorg.civicrm.newyork/astoria/css/dashboard.css
civicrm-core/ang/crmMailing.cssorg.civicrm.newyork/astoria/ang/crmMailing.css
org.civicrm.volunteer/css/main.cssorg.civicrm.newyork/astoria/org.civicrm.volunteer-css/dashboard.css
org.civicrm.rules/style/admin.cssorg.civicrm.newyork/astoria/org.civicrm.rules-style/admin.css