Overview
Larapen's theme system allows you to create fully custom front-end designs. Each theme is a self-contained package with its own views, stylesheets, JavaScript, and configuration. This guide walks you through creating a theme from scratch.
Prerequisites
- PHP 8.3+ and a working Larapen installation.
- Node.js 18+ and npm installed (for asset compilation via Vite).
- Basic knowledge of Laravel Blade templating, Bootstrap 5.3, and SCSS.
- A code editor (VS Code, PhpStorm, etc.).
Step 1: Create the theme directory
All themes live under extensions/themes/. Create a directory for your theme using a lowercase, URL-safe name:
extensions/themes/my-theme/
Step 2: Create the manifest (theme.json)
Create extensions/themes/my-theme/theme.json: this file tells Larapen about your theme:
{
"name": "my-theme",
"display_name": "My Custom Theme",
"version": "1.0.0",
"author": "Your Name",
"license_type": "free",
"description": "A custom theme for my website",
"thumbnail": "assets/images/thumbnail.png",
"supports": ["pages", "portfolio", "blog", "shop"],
"settings": {
"primary_color": "#3b82f6",
"layout_style": "wide"
},
"item_id": ""
}
Manifest field reference
| Field | Type | Description |
|---|---|---|
name | string | Required. Unique machine name: must match directory name. |
display_name | string | Required. Human-readable name shown in admin panel. |
version | string | Semantic version (e.g., 1.0.0). |
author | string | Author name. |
license_type | string | "free" or "premium". |
description | string | Short description for the admin panel. |
thumbnail | string | Relative path to a preview screenshot (recommended: 1200×900px). |
supports | array | Feature flags: pages, portfolio, blog, shop. |
settings | object | Default setting values (overridden by config.php). |
item_id | string | Envato item ID. Leave empty for non-marketplace themes. |
Step 3: Create the settings file (config.php)
Create extensions/themes/my-theme/config.php: this defines all customizable theme settings with their default values:
<?php
return [
// Colors
'primary_color' => '#3b82f6',
'secondary_color' => '#1e40af',
'accent_color' => '#f59e0b',
'text_color' => '#1e293b',
// Typography
'heading_font' => 'DM Sans',
'body_font' => 'Inter',
'base_font_size' => '16px',
// Layout
'layout_style' => 'wide', // 'wide' | 'boxed'
'container_width' => '1320px',
'wide_container_width' => '1250px',
// Header
'header_wide' => false,
'header_style' => 'default',
'header_height' => '',
'header_bg_color' => '',
'header_bg_image' => '',
'header_text_color' => '',
'header_btn_label_color' => '',
'header_btn_bg_color' => '',
'header_text_shadow' => false,
'header_logo_height' => '',
'header_border_bottom_width' => '',
'header_border_bottom_color' => '',
// Footer
'footer_style' => 'default',
'footer_bg_color' => '',
'footer_bg_image' => '',
'footer_title_color' => '',
'footer_text_color' => '',
'footer_link_color' => '',
'footer_border_top_width'=> '',
'footer_border_top_color'=> '',
// UI Behavior
'show_back_to_top' => true,
'default_mode' => 'light', // 'light' | 'dark'
'allow_mode_toggle' => true,
// Custom code
'custom_css' => '',
'custom_js' => '',
];
These settings are editable by the admin at Appearance → Theme Settings and are merged with any overrides stored in the database. You can add your own custom settings keys here; they will be available in all theme views via the $themeSettings array.
Step 4: Create the directory structure
Here is the recommended directory structure for a complete theme:
extensions/themes/my-theme/
├── theme.json
├── config.php
├── assets/
│ ├── images/
│ │ └── thumbnail.png
│ ├── scss/
│ │ ├── theme.scss ← Main SCSS entry (REQUIRED)
│ │ └── pages/ ← Page-specific SCSS (optional)
│ │ ├── home.scss
│ │ ├── contact.scss
│ │ ├── portfolio.scss
│ │ └── ...
│ └── js/
│ ├── theme.js ← Main JS entry (REQUIRED)
│ └── pages/ ← Page-specific JS (optional)
│ ├── home.js
│ └── ...
└── views/
├── layouts/
│ └── master.blade.php ← Main layout (REQUIRED)
├── partials/
│ ├── header.blade.php
│ ├── footer.blade.php
│ └── seo.blade.php
├── sections/ ← Section templates (for page builder)
│ ├── hero.blade.php
│ ├── features.blade.php
│ └── ...
├── components/ ← Reusable Blade components
│ └── page-header.blade.php
└── pages/ ← Full page views
├── home.blade.php
├── page.blade.php
├── sectioned.blade.php
├── contact.blade.php
├── portfolio/
│ ├── index.blade.php
│ └── show.blade.php
└── auth/
├── login.blade.php
└── register.blade.php
theme.json, assets/scss/theme.scss, assets/js/theme.js, and views/layouts/master.blade.php. Without these, the theme cannot be activated.
Step 5: Create the master layout
The master layout (views/layouts/master.blade.php) is the HTML shell for every page. Here is a minimal starting point:
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}"
dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}"
data-bs-theme="{{ $themeSettings['default_mode'] ?? 'light' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
@yield('seo')
{{-- Google Fonts --}}
@php
$headingFont = $themeSettings['heading_font'] ?? 'DM Sans';
$bodyFont = $themeSettings['body_font'] ?? 'Inter';
$families = collect([$headingFont, $bodyFont])
->unique()->map(fn ($f) => urlencode($f) . ':wght@300;400;500;600;700')
->implode('&family=');
@endphp
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family={{ $families }}&display=swap"
rel="stylesheet">
{{-- Theme CSS variables --}}
<style>
:root {
--color-primary: {{ $themeSettings['primary_color'] ?? '#3b82f6' }};
--color-secondary: {{ $themeSettings['secondary_color'] ?? '#1e40af' }};
--color-accent: {{ $themeSettings['accent_color'] ?? '#f59e0b' }};
--font-heading: '{{ $headingFont }}', sans-serif;
--font-body: '{{ $bodyFont }}', sans-serif;
}
</style>
{{-- Vite assets (update path to match YOUR theme name) --}}
@vite([
'extensions/themes/my-theme/assets/scss/theme.scss',
'extensions/themes/my-theme/assets/js/theme.js'
])
@stack('styles')
</head>
<body>
@include('partials.header')
<main>
@yield('content')
</main>
@include('partials.footer')
@stack('scripts')
</body>
</html>
Available variables in all views
| Variable | Type | Description |
|---|---|---|
$themeSettings | array | All theme settings (merged config.php defaults + admin overrides). |
$activeTheme | string | Name of the currently active theme. |
$isPreviewMode | bool | true when using ?theme= query parameter for preview. |
$currentLocale | string | Current language code (e.g., en, fr). |
$availableLocales | Collection | All available languages for the language switcher. |
Step 6: Create the main SCSS file
Create assets/scss/theme.scss: this is the main stylesheet entry point:
// 1. Override Bootstrap variables BEFORE importing Bootstrap
$primary: var(--color-primary);
$secondary: var(--color-secondary);
// 2. Import Bootstrap (full build)
@import "bootstrap/scss/bootstrap";
// 3. Import Bootstrap Icons
@import "bootstrap-icons/font/bootstrap-icons.css";
// 4. Your custom theme styles
:root {
--font-heading: 'DM Sans', sans-serif;
--font-body: 'Inter', sans-serif;
}
body {
font-family: var(--font-body);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
}
// Add your custom component styles, animations, etc.
Step 7: Create the main JavaScript file
Create assets/js/theme.js: this is the main JS entry point:
// Import Bootstrap JS
import * as bootstrap from 'bootstrap';
// Import shared core utilities
import '../../../../resources/js/confirm-dialog.js';
import '../../../../resources/js/select2-init.js';
import '../../../../resources/js/lightbox.js';
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
initHeader();
initMobileMenu();
initThemeToggle();
// ... your custom initializations
});
function initHeader() {
const header = document.querySelector('.site-header');
if (!header) return;
window.addEventListener('scroll', () => {
header.classList.toggle('scrolled', window.scrollY > 50);
});
}
function initMobileMenu() {
// Your mobile menu toggle logic
}
function initThemeToggle() {
// Dark/light mode toggle logic
const toggle = document.querySelector('[data-theme-toggle]');
if (!toggle) return;
toggle.addEventListener('click', () => {
const html = document.documentElement;
const current = html.getAttribute('data-bs-theme');
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-bs-theme', next);
localStorage.setItem('theme-preference', next);
});
}
Step 8: Create page views
Page views go in views/pages/ and extend the master layout:
{{-- views/pages/home.blade.php --}}
@extends('layouts.master')
@section('seo')
@include('partials.seo', ['seo' => $seo ?? []])
@endsection
@section('content')
{{-- Hero section --}}
<section class="py-5 bg-primary text-white">
<div class="container text-center">
<h1>{{ $page->title }}</h1>
<p class="lead">{{ $page->excerpt }}</p>
</div>
</section>
{{-- Page content --}}
<section class="py-5">
<div class="container">
{!! $page->content !!}
</div>
</section>
@endsection
@push('styles')
@vite('extensions/themes/my-theme/assets/scss/pages/home.scss')
@endpush
Step 9: Override add-on views
Your theme can override views from any add-on by placing files in the matching path. For example, to override the blog listing page:
extensions/themes/my-theme/views/pages/blog/index.blade.php
extensions/themes/my-theme/views/pages/shop/index.blade.php
extensions/themes/my-theme/views/pages/faq/index.blade.php
The theme view resolution order is:
- Active theme:
extensions/themes/{active-theme}/views/{view} - Default theme fallback:
extensions/themes/default/views/{view} - Standard Laravel:
resources/views/{view}
Step 10: Build assets with Vite
Larapen's Vite configuration automatically discovers theme assets. After creating your SCSS and JS files, build them:
# Build all theme assets
npx vite build
# Or use the Artisan command to build a specific theme
php artisan theme:build my-theme
npx vite build after creating any new SCSS or JS files. The Vite manifest (public/build/manifest.json) must be rebuilt for @vite() directives to resolve your theme's assets.
Vite auto-discovery
Vite automatically picks up these file patterns from your theme:
| Pattern | Purpose |
|---|---|
assets/scss/theme.scss | Main stylesheet entry point |
assets/js/theme.js | Main JavaScript entry point |
assets/scss/pages/*.scss | Per-page stylesheets |
assets/js/pages/*.js | Per-page JavaScript |
Step 11: Activate your theme
- Go to Appearance → Themes in the admin panel.
- Click "Sync" to discover new themes from the filesystem.
- Your theme should appear in the list. If assets are not built yet, you'll see a "Build" button.
- Click "Activate" to switch to your theme.
Step 12: Publish static assets (optional)
If your theme has static assets (images, fonts) that need to be publicly accessible outside of Vite:
php artisan theme:publish my-theme
This copies extensions/themes/my-theme/assets/ to public/themes/my-theme/. Reference them in Blade using:
<img src="{{ theme_asset('images/logo.png') }}" alt="Logo">
Section templates
If your site uses the page builder (sectioned pages), your theme should include section templates in views/sections/. The page builder supports 40+ section types organized into categories:
| Category | Section Types |
|---|---|
| Layout | Hero, CTA, Divider, Spacer |
| Content | Text, Rich Text, Quote, Accordion, Tabs, Timeline |
| Titles | Section Title, Page Title, Subtitle |
| Media | Image, Gallery, Video, Carousel, Before/After |
| Data | Features, Stats, Pricing, Comparison Table, Icon Grid |
| Engagement | Testimonials, Team, Partners, FAQ, Contact Form, Newsletter |
| Embed | HTML, Map, Social Feed, Code Block |
| Add-ons | Blog Posts, Products, Portfolio, Events (if add-ons are active) |
Each section type needs its own Blade partial. The section renderer passes these variables:
| Variable | Description |
|---|---|
$section | The section model with all its configured data. |
$resolvedData | Pre-resolved data (e.g., fetched portfolio items, team members). |
Per-theme styling overrides
Each section supports per-theme styling overrides: admins can customize 14 CSS properties per section per theme without modifying the section content. Your theme's section templates automatically receive these styling overrides applied via the section's style attributes.
Useful Artisan commands
| Command | Description |
|---|---|
php artisan theme:status | Shows all themes with build status (source files, built, active). |
php artisan theme:build {name} | Builds a specific theme's assets via Vite. |
php artisan theme:publish {name} | Publishes static assets to public/themes/. |
Using AI to create themes
Larapen includes an MCP (Model Context Protocol) server that lets AI assistants understand the theme system and generate valid theme files. Instead of creating files manually, you can describe your theme in plain language and the AI will scaffold all necessary files following Larapen conventions. See the "Using AI to Create Themes" article for detailed setup instructions and workflows.
Tips for high-quality themes
- Responsive first: Design mobile-first and use Bootstrap's responsive grid and breakpoints.
- Use CSS custom properties: Define all colors and fonts as
:rootvariables so admins can customize easily. - Leverage Bootstrap utilities: Minimize custom CSS by using Bootstrap's spacing, flexbox, typography, and color utilities.
- Accessible: Follow WCAG AA guidelines: proper heading hierarchy, color contrast, keyboard navigation,
altattributes. - Dark mode support: Use Bootstrap's
data-bs-theme="dark"attribute for automatic dark mode support. - Translatable strings: Never hardcode text. Always use
{{ __('key') }}for all visible strings. - Distinctive design: Each theme should have its own visual identity: unique font pairings, color palette, and layout philosophy.