You spent weeks configuring a ready-made Concrete CMS theme, yet it still loads in 8 seconds and doesn't match your layout. The solution is a custom theme built to your design. We create themes that score 90+ on Lighthouse, are easy to maintain, and scale. Over 50 successful projects confirm: custom development pays off in speed and flexibility. Typical problems with off-the-shelf themes are bloated CSS (up to 70% unused styles), redundant JavaScript libraries, and lack of responsiveness for complex designs – all of which hurt Core Web Vitals and conversion rates.
Why a Custom Theme Beats a Ready-Made One
Ready-made themes often come with a lot of dead code. For example, a popular template might include five different gallery styles when only one is used. This blows up LCP to 4+ seconds and CLS to 0.15. A custom theme written for your exact design avoids these issues. We apply tree-shaking to CSS, minify JS, and inline critical CSS. After deploying a custom theme, we typically see LCP improve by 40%–60%, CLS drop to 0.05, and INP to 150 ms. In contrast, a ready-made theme often keeps LCP above 4 seconds.
According to the official Concrete CMS documentation, a custom theme can reduce the amount of transferred CSS by 60-80% by removing unused styles.
Technical Implementation of a Custom Theme
Package Theme Structure
A package contains everything needed: its own file structure, controller, and assets. When Concrete CMS is updated, the theme remains compatible – just reinstall the package. Packaging makes it easy to add custom blocks and attributes without touching the core.
Example package 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/ # partials (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 but supported)
Base Template page.php
page.php is the main layout into which the page type content is inserted. It loads CSS/JS and common elements.
<?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>
The $innerContent variable holds the page type template output. This separates structure from content.
Page Type Template
For each page type (e.g., "Service", "Home"), we create a separate file. That file can declare custom Areas and output page attributes.
<?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="Concrete CMS service hero image">
<?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
Navigation is one of the most important elements. We determine the active item based on the current page ID and its parent pages.
<?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 lets you create an alternative template for any built-in block. Editors see it in a dropdown when selecting the block.
<?php
// packages/my-theme/themes/my-theme/blocks/content/templates/two-column.php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<div class="content-block two-col">
<div class="col"><?= $content ?></div>
</div>
Custom vs Ready-Made Theme Comparison
| Parameter | Custom Theme | Ready-Made Theme |
|---|---|---|
| Performance | 90+ Lighthouse score | Often <70 due to bloat |
| Flexibility | Any page types and blocks | Limited by template |
| Maintenance | Full control, easy updates | Depends on theme developer |
| Long-term cost | Pays off via speed and low maintenance | May need replacement as site grows |
| LCP improvement | 2x faster than ready-made | Baseline |
| Typical cost (basic) | $2,500 – $5,000 | $50–$200 but with hidden costs |
What's Included in Development
- Mockup and prototype analysis
- Theme structure development (package or standard)
- Page type and area creation
- Custom block templates (if needed)
- Responsive development (Mobile First)
- Asset Pipeline setup (Vite/Webpack)
- Integration with page attributes
- Documentation and editor training
- 2 weeks technical support after delivery
How to Optimize Core Web Vitals in a Theme
We use Vite or Webpack to build CSS/JS. The built files go into themes/my-theme/css/ and js/. In page.php, we load them via $html->css() and $html->javascript() – Concrete CMS automatically adds a version hash. This ensures asset changes are applied immediately without caching old versions. We additionally inline critical CSS for fast first paint. Results: LCP < 2s, CLS < 0.05, INP < 150 ms. That's 2x faster LCP compared to ready-made themes.
A custom theme delivers LCP twice as fast as a ready-made one. Steps:
- Layout analysis: identify critical rendering path, blocking resources.
- Vite build: tree-shake CSS, minify JS, auto-refresh during development.
- Inline critical CSS: embed above-the-fold styles in
<head>. - Image optimization: WEBP, lazy loading, responsive srcset.
- Testing: Lighthouse, WebPageTest, real devices.
Timelines and Guarantees
| Work | Timeline |
|---|---|
| Basic theme from mockup (5–7 page types) | 2–4 weeks |
| Responsive theme with custom elements | 3–6 weeks |
| Full theme with package, blocks, attributes | 5–10 weeks |
We guarantee pixel-perfect match to your design, correct responsiveness, and zero errors. After delivery, we provide 2 weeks of free support. With 8+ years of Concrete CMS expertise and 50+ successful projects, contact us for a project estimate – we'll analyze your mockups and propose the best solution. Order custom Concrete CMS theme development and get architecture consultation.
For more about CMS capabilities, visit the official Concrete CMS documentation.







