Skip to content

Creating Custom Themes

Create a custom theme as a Capell package when it should be installable, testable, and reusable across apps. Keep one-off project styling in the app only when it depends on private app code.

A theme package should include:

  • composer.json
  • capell.json
  • a service provider
  • a ThemeDefinitionData registration
  • one ThemeRenderer
  • section renderers for the sections the package owns
  • public CSS/JS assets registered from PHP
  • preset definitions with preview metadata
  • package tests for manifest, renderer registration, presets, preview image, and public-output safety

Themes that extend Foundation should declare the parent through package metadata and keep only the changed sections/assets in the child package.

Register with ThemeRegistry from the package service provider:

use Capell\Core\Enums\FrontendRuntime;
use Capell\Core\ThemeStudio\Data\ThemeDefinitionData;
use Capell\Core\ThemeStudio\Data\ThemePresetData;
use Capell\Core\ThemeStudio\Theme\ThemeRegistry;
public function boot(ThemeRegistry $themes): void
{
$themes->register(
definition: new ThemeDefinitionData(
key: 'agency-launch',
name: 'Agency Launch',
description: 'Portfolio and lead generation theme for service businesses.',
package: 'capell-app/theme-agency-launch',
previewImage: '/vendor/capell/theme-agency-launch/preview.jpg',
tags: ['portfolio', 'lead-generation'],
bestFit: ['agency', 'consulting'],
includedSections: ['navigation', 'hero', 'features', 'footer'],
presets: [
new ThemePresetData(
key: 'editorial-warmth',
name: 'Editorial Warmth',
description: 'Warm accent palette with editorial spacing.',
previewImage: '/vendor/capell/theme-agency-launch/presets/editorial-warmth.jpg',
values: [
'primaryColor' => '#0f766e',
'accentColor' => '#f59e0b',
'radius' => 'sm',
],
),
],
assets: [
'frontend' => '/vendor/capell/theme-agency-launch/theme.css',
],
runtime: FrontendRuntime::Livewire,
extends: 'foundation',
),
themeRenderer: new AgencyLaunchThemeRenderer,
sectionRenderers: [
new HeroSectionRenderer,
new FeaturesSectionRenderer,
],
);
}

Keep key stable. Installed themes.key, preview tokens, cache keys, and diagnostics all rely on it.

Presets are explicit choices, not hidden defaults. Each preset needs:

  • stable key
  • human label
  • short description
  • preview image path
  • token values using the shared vocabulary in Frontend themes

Package-specific values are allowed, but the renderer must handle missing or unknown values safely.

Package themes can extend the admin Theme Editor by binding a class that implements Capell\Admin\Contracts\Themes\ThemeEditorExtension and tagging it with ThemeEditorExtension::TAG.

Use an extension when a package needs:

  • extra editor sections or fields
  • package-specific preview sample content
  • a custom preview Blade component
  • extra CSS variables or data attributes derived from editor state

The extension receives a ThemeEditorContextData instance so it can decide whether it supports the current theme. Keep editor-only values under the clean editor state shape:

  • meta.editor.preset.active
  • meta.editor.brand.*
  • meta.editor.header.*
  • meta.editor.surface.*
  • meta.editor.footer.*
  • meta.editor.assets.*
  • meta.editor.advanced.*
  • admin.editor.*

Do not read old flat meta or admin editor fields from new extension code. Legacy fields remain compatibility data only.

use Capell\Admin\Contracts\Themes\ThemeEditorExtension;
use Capell\Admin\Data\Themes\ThemeEditorContextData;
use Capell\Admin\Data\Themes\ThemeEditorStateData;
final class AgencyLaunchThemeEditorExtension implements ThemeEditorExtension
{
public function supports(ThemeEditorContextData $context): bool
{
return $context->themeKey === 'agency-launch';
}
public function editorSections(ThemeEditorContextData $context): array
{
return [];
}
public function samplePreviewContent(ThemeEditorContextData $context): array
{
return [
'headline' => 'Launch campaigns with reusable sections.',
'body' => 'Preview copy should show the package sections without querying public models.',
];
}
public function previewComponent(ThemeEditorContextData $context): ?string
{
return null;
}
public function cssVariables(ThemeEditorStateData $state, ThemeEditorContextData $context): array
{
return [
'--agency-launch-accent' => $state->brand['accentColor'] ?? '#f59e0b',
];
}
public function dataAttributes(ThemeEditorStateData $state, ThemeEditorContextData $context): array
{
return [
'theme-package' => $context->themeKey,
];
}
}

Register it from the package service provider:

$this->app->tag(AgencyLaunchThemeEditorExtension::class, ThemeEditorExtension::TAG);

Theme renderers receive prepared ThemePageData. They should render from that data and registered section renderers.

Do not query models from public Blade views. Load public render data before the view receives it, or add an explicit view component/action that prepares the data.

Do not print authoring metadata. Public HTML must be safe for anonymous visitors, signed-in users, admins, crawlers, cache files, and static exports.

Register package asset sources from PHP:

  • Tailwind source paths through TailwindAssetsRegistry
  • CSS/JS dependencies through the package provider or the package’s frontend asset registration code
  • token-compatible CSS through ThemeTokenStore output, not hard-coded critical CSS paths

The current direction is to use the optional frontend optimizer package for generated critical CSS. Do not add a theme-level Beasties/Critters fallback.

Run the authoring validation command in the app:

Terminal window
php artisan capell:themes:validate agency-launch

Package tests should cover:

  • capell.json and Composer metadata are valid
  • provider registers the theme definition and renderer
  • all advertised presets resolve
  • preview images are present in the package/public asset path
  • section renderers cover the declared sections or inherit safely from Foundation
  • public rendered output contains expected frontend HTML
  • public rendered output does not expose authoring metadata

Use a route-backed frontend test for at least one seeded page. A renderer-only unit test is useful, but it will not catch package boot, asset, cache, or public safety regressions.