Concrete CMS Custom Theme Development
A theme in Concrete CMS is a directory with PHP templates, CSS, and JS. Theme can be shipped as folder in application/themes/ or as part of package in packages/my-theme/themes/. Package approach is preferable — allows distributing and versioning theme with all dependencies.
Theme Structure
packages/my-theme/
controller.php # package controller
themes/
my-theme/
page_types/ # page type templates
home.php
default.php
service-detail.php
elements/ # template parts (header, footer, nav)
header.php
footer.php
navigation.php
blocks/ # block template overrides
content/
templates/
two-column.php
css/
main.css
js/
app.js
thumbnail.png # theme preview (360×270)
description.txt
page.php # base template (fallback)
view.php # main layout (deprecated, supported)
Base Template page.php
page.php is the main layout where page type content is inserted:
<?php
// packages/my-theme/themes/my-theme/page.php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<!DOCTYPE html>
<html lang="<?= Localization::activeLocale() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $c->getCollectionName() ?> | <?= $site->getName() ?></title>
<?php
$html = Loader::helper('html');
echo $html->css('main.css', 'my-theme');
?>
<?php View::element('header_required', ['disableTrackingCode' => false]) ?>
</head>
<body class="<?= $c->getPageWrapperClass() ?>">
<?php View::element('header', [], 'my-theme') ?>
<main id="main-content">
<?php echo $innerContent ?>
</main>
<?php View::element('footer', [], 'my-theme') ?>
<?php echo $html->javascript('app.js', 'my-theme') ?>
<?php View::element('footer_required') ?>
</body>
</html>
$innerContent is variable where Concrete CMS inserts page type template content.
Page Type Template
<?php
// page_types/service-detail.php
defined('C5_EXECUTE') or die('Access Denied.');
$a_hero = new Area('Hero');
$a_content = new Area('Main Content');
$a_related = new Area('Related Services');
$heroImage = $c->getAttribute('hero_image');
$intro = $c->getAttribute('intro_text');
?>
<section class="hero <?= $heroImage ? 'hero--has-image' : '' ?>">
<?php if ($heroImage): ?>
<img src="<?= $heroImage->getURL() ?>" alt="<?= h($c->getCollectionName()) ?>">
<?php endif; ?>
<div class="hero__content">
<h1><?= h($c->getCollectionName()) ?></h1>
<?php if ($intro): ?><p><?= h($intro) ?></p><?php endif; ?>
</div>
</section>
<div class="container layout-sidebar">
<article class="content">
<?php $a_content->display($c) ?>
</article>
<aside class="sidebar">
<?php $a_related->display($c) ?>
</aside>
</div>
Navigation Element with Active State
<?php
// elements/navigation.php
defined('C5_EXECUTE') or die('Access Denied.');
$navHelper = Loader::helper('navigation');
$pages = Page::getByPath('/')->getCollectionChildren();
?>
<nav class="main-nav">
<ul>
<?php foreach ($pages as $navPage): ?>
<?php
$isActive = ($c->getCollectionID() == $navPage->getCollectionID()
|| $navHelper->isParentPage($c, $navPage));
?>
<li class="<?= $isActive ? 'active' : '' ?>">
<a href="<?= $navPage->getCollectionLink() ?>">
<?= h($navPage->getCollectionName()) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
Block Template Override
Concrete CMS allows creating alternative template for any built-in block:
// packages/my-theme/themes/my-theme/blocks/content/templates/two-column.php
// Alternative template for Content block (HTML)
defined('C5_EXECUTE') or die('Access Denied.');
?>
<div class="content-block two-col">
<div class="col"><?= $content ?></div>
</div>
Editor sees this template in dropdown when selecting Content block.
Automatic Theme Configuration File
<?php
// themes/my-theme/page_theme.php — registration of page types and areas
namespace Application\Theme\MyTheme;
use Concrete\Core\Page\Theme\Theme;
class PageTheme extends Theme {
public function getThemeName(): string { return 'My Theme'; }
public function getThemeDescription(): string { return 'Corporate theme'; }
public function registerAssets(): void {
$this->providesAsset('css', 'main.css');
$this->providesAsset('javascript', 'app.js');
$this->requireAsset('javascript', 'jquery');
}
}
Responsiveness and Asset Pipeline
For CSS/JS compilation use Vite or Webpack, output to themes/my-theme/css/ and js/. In page.php include via $html->css() / $html->javascript() — Concrete CMS adds version hash automatically.
Theme Development Timeline
| Work | Timeline |
|---|---|
| Basic theme from layout (5–7 page types) | 2–4 weeks |
| Responsive theme with custom elements | 3–6 weeks |
| Full theme with package, blocks, attributes | 5–10 weeks |







