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.







