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.

Don't want to build it yourself? BeDigit, the company behind Larapen, offers custom theme development services. Request a quote and our team will design and build a custom theme tailored to your brand.

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

FieldTypeDescription
namestringRequired. Unique machine name: must match directory name.
display_namestringRequired. Human-readable name shown in admin panel.
versionstringSemantic version (e.g., 1.0.0).
authorstringAuthor name.
license_typestring"free" or "premium".
descriptionstringShort description for the admin panel.
thumbnailstringRelative path to a preview screenshot (recommended: 1200×900px).
supportsarrayFeature flags: pages, portfolio, blog, shop.
settingsobjectDefault setting values (overridden by config.php).
item_idstringEnvato 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
Required files: At minimum, your theme must have 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

VariableTypeDescription
$themeSettingsarrayAll theme settings (merged config.php defaults + admin overrides).
$activeThemestringName of the currently active theme.
$isPreviewModebooltrue when using ?theme= query parameter for preview.
$currentLocalestringCurrent language code (e.g., en, fr).
$availableLocalesCollectionAll 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.
Tip: Use Bootstrap 5.3 utility classes as much as possible. Only write custom SCSS for styles that can't be achieved with utilities. This keeps your theme lightweight and maintainable.

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);
    });
}
Important: Larapen uses vanilla JavaScript only: no jQuery, React, or Vue. Use native DOM APIs and ES2022+ features.

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:

  1. Active theme: extensions/themes/{active-theme}/views/{view}
  2. Default theme fallback: extensions/themes/default/views/{view}
  3. 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
Critical: You MUST run 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:

PatternPurpose
assets/scss/theme.scssMain stylesheet entry point
assets/js/theme.jsMain JavaScript entry point
assets/scss/pages/*.scssPer-page stylesheets
assets/js/pages/*.jsPer-page JavaScript

Step 11: Activate your theme

  1. Go to Appearance → Themes in the admin panel.
  2. Click "Sync" to discover new themes from the filesystem.
  3. Your theme should appear in the list. If assets are not built yet, you'll see a "Build" button.
  4. 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:

CategorySection Types
LayoutHero, CTA, Divider, Spacer
ContentText, Rich Text, Quote, Accordion, Tabs, Timeline
TitlesSection Title, Page Title, Subtitle
MediaImage, Gallery, Video, Carousel, Before/After
DataFeatures, Stats, Pricing, Comparison Table, Icon Grid
EngagementTestimonials, Team, Partners, FAQ, Contact Form, Newsletter
EmbedHTML, Map, Social Feed, Code Block
Add-onsBlog Posts, Products, Portfolio, Events (if add-ons are active)

Each section type needs its own Blade partial. The section renderer passes these variables:

VariableDescription
$sectionThe section model with all its configured data.
$resolvedDataPre-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

CommandDescription
php artisan theme:statusShows 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 :root variables 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, alt attributes.
  • 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.
Need help? If you'd prefer professional assistance, BeDigit offers custom theme development services. Request a free quote →

Was this article helpful?

Thank you for your feedback!

Still need help? Create a support ticket

Create a Ticket