Theme Reference¶
Introduction¶
CiviCRM supports CSS-based theming. You may define a theme which overrides
or supplements a CSS file such as civicrm.css
and bootstrap.css
.
Most installations of CiviCRM are embedded within a CMS (such as Drupal, Joomla, or WordPress). Customizing the look-and-feel therefore includes two general areas of work:
- CMS Theming: Use the features and conventions of the CMS to manage the general site-wide appearance.
- Civi Theming: Use the features and conventions of Civi to tune the appearance of Civi's screens.
As the CiviCRM application continues to evolve, the intention is to align these two areas by adopting Bootstrap CSS throughout CiviCRM. A site-builder (or theme-developer) will select (or write) a Bootstrap-based theme for their CMS -- and this will define the look-and-feel within Civi's screens.
However, we cannot achieve that vision in one dramatic leap. Rather, theming in Civi will be in transition for the foreseeable future, so we must discuss some more nuanced concepts. The key concepts in Civi theming are:
- Look-and-Feel (Theme): The visual appearance that an end-user will recognize. (Ex: a flat, high-contrast appearance built around blue and white)
- Package: The deliverable that an administrator installs/activates. (Ex: a CiviCRM extension)
- Vocabulary: The set of HTML/CSS class-names which form a contract between HTML and CSS providers. (Ex: BootstrapCSS)
Generally speaking, these are relevant to people acting in different roles:
- A theme developer who wishes to define a new look-and-feel (theme) should learn about the vocabulary by examining its style guide; then, he or she can create a package (e.g. CiviCRM extension) which includes a CSS file for the vocabulary.
- A site administrator who wishes to change the look-and-feel of the site should install the package provided by the theme developer.
- An application developer who wishes to provide a pleasant experience should learn about the vocabulary by examining its style guide. He or she can create pages which load the CSS file and use this vocabulary.
We can explore each of these concepts in greater detail.
Quick Start¶
To generate an extension with a new, skeletal theme named newyork
, use civix:
$ civix generate:module org.civicrm.newyork
$ cd org.civicrm.newyork
$ civix generate:theme
Initialize theme "newyork"
Write newyork.theme.php
Write css/civicrm.css
Write css/bootstrap.css
Note how this provides theme metadata (newyork.theme.php
) and two empty CSS files (css/civicrm.css
and
css/bootstrap.css
).
Themes¶
A theme determines the visual appearance that an end-user will recognize. At time of writing, there are two important themes in CiviCRM:
- Greenwich (Village): This has been the standard appearance since roughly CiviCRM 3.x. Visually, you can recognize Greenwich by the use of grey beveled buttons. Technically, it has been developed in vanilla CSS.
- Shoreditch: This is a newer theme developed as an extension during 4.7/5.x. Visually, you can recognize Shoreditch by its flat design. Technically, it has been developed in the Bootstrap(S)CSS framework.
What's in a name?
During a planning session for Shoreditch in Fort Collins, CO, we got repeatedly confused by the lack of a distinct name for the current and proposed themes. We started to use names that paid hommage to the original development teams -- naming each after a neighborhood that was close to the original teams' bases of operations.
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.
Shoreditch as bridge
At time of writing, Shoreditch is the only theme which implements
a consistent look-and-feel in both vocabularies. It is intended to be a bridge that provides a consistent
look-and-feel while other parts of the application transition from crm-*
to BootstrapCSS.
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
).
Packaging¶
Packaging is the process of putting the CSS file(s) into a deliverable format that can be installed/activated by an administator. CiviCRM supports many package types, such as "CiviCRM extensions", "Drupal modules", and "WordPress plugins".
CiviCRM 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
Drupal/Joomla/WordPress¶
In principle, you may package a theme using anything that supports CiviCRM hooks -- just implement
hook_civicrm_themes
. At time of writing, this technique has not actually been used yet, but a few tips may help:
- Define the extension key: The previous example defines a
fully-qualified exension-key (
ext
) with valueorg.civicrm.newyork
. For other packages, the naming convention is different. For example, a Drupal module namedfoobar
would have the extension-keydrupal.foobar
. (These prefixes are not frequently used; we may encounter bugs when using different prefixes. Patchwelcome.) - Exclude Files: If the the CMS theme already loads a copy of
bootstrap.css
through the CMS, then it may be redundant to load a copy ofbootstrap.css
through Civi's theming layer. See "Advanced: excludes". - Define a callback: When loading a CSS file such as
civicrm.css
, the default loader tries to read it from your package. However, if your package has a different file structure (or if there's a bug in locating your package's folder), you should define a custom callback function. See "Advanced: url_callback".
Legacy¶
Prior to CiviCRM v5.8, Civi did not support hook_civicrm_themes
. Instead, you could
manually deploy CSS files and then configure some settings:
customCSSURL
: Loads an extra CSS file on every CiviCRM screen.disable_core_css
: Disables the standard call toaddStyleFile('civicrm', 'css/civicrm.css')
.
However, these settings have notable limitations which spurred the development of
hook_civicrm_themes
:
- They don't provide a full packaging format. When defining a full, cogent look-and-feel,
it's useful to bundle related files (e.g.
civicrm.css
,bootstrap.css
, images, icons). This makes it easier to share/redistribute/collaborate on the new look-and-feel. - They apply to all CiviCRM screens equally. If you want to provide a different look-and-feel on different screens (e.g. frontend vs backend), there's no way to selectively adjust these options.
For new theming projects, it's better to package the customization using an extension (or Drupal module, WordPress plugin, ad nauseum). However, for existing projects, the settings will continue to work.
Advanced¶
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 |