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-*(akacivicrm.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(...)orCivi::resources()->getUrl(...) - Generate files in a datadir using
Civi::paths()->getPath(...)orCivi::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 File | Theme File |
civicrm-core/css/dashboard.css | org.civicrm.newyork/css/dashboard.css |
civicrm-core/ang/crmMailing.css | org.civicrm.newyork/ang/crmMailing.css |
org.civicrm.volunteer/css/main.css | org.civicrm.newyork/org.civicrm.volunteer-css/dashboard.css |
org.civicrm.rules/style/admin.css | org.civicrm.newyork/org.civicrm.rules-style/admin.css |
If you use a multitheme/prefixed configuration, then theme prefixes apply accordingly.
| Original File | Theme File |
civicrm-core/css/dashboard.css | org.civicrm.newyork/astoria/css/dashboard.css |
civicrm-core/ang/crmMailing.css | org.civicrm.newyork/astoria/ang/crmMailing.css |
org.civicrm.volunteer/css/main.css | org.civicrm.newyork/astoria/org.civicrm.volunteer-css/dashboard.css |
org.civicrm.rules/style/admin.css | org.civicrm.newyork/astoria/org.civicrm.rules-style/admin.css |