The Premium Theme is designed to be highly customizable while maintaining code quality and performance. This guide covers advanced customization techniques beyond the basic settings available in the admin interface.
The theme uses a modular CSS architecture with Tailwind CSS:
styles/src/
├── main.css # Entry point
├── base/
│ ├── variables.css # CSS custom properties
│ └── reset.css # Normalizations
├── components/
│ ├── buttons.css # Button styles
│ ├── cards.css # Card components
│ ├── forms.css # Form elements
│ ├── navigation.css # Navigation styles
│ └── article.css # Article-specific
├── layouts/
│ ├── containers.css # Container system
│ └── grid.css # Grid layouts
└── utilities/
├── typography.css # Text utilities
└── spacing.css # Spacing helpers
The easiest way to customize colors, fonts, and spacing is through CSS custom properties (variables).
Add to Custom CSS field in admin:
:root {
/* Override primary hue */
--primary-hue: 200; /* Custom blue */
/* Override spacing */
--space-4: 1.25rem; /* Increase base spacing */
/* Override fonts */
--user-font-heading: 'Playfair Display', serif;
--user-fonT-body: 'Source Sans Pro', sans-serif;
/* Override border radius */
--radius-md: 0.5rem;
--radius-lg: 1rem;
}Override specific component styles:
/* Customize article cards */
.article-card {
border: 2px solid var(--color-primary-200);
border-radius: var(--radius-xl);
}
.article-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-xl);
}
/* Customize buttons */
.btn-primary {
background: linear-gradient(135deg,
hsl(var(--primary-hue), 70%, 50%),
hsl(var(--primary-hue), 70%, 40%)
);
}
/* Customize navigation */
.main-nav {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}Customize dark mode specifically:
[data-theme="dark"] {
/* Override dark mode colors */
--color-bg: hsl(220, 25%, 8%);
--color-bg-secondary: hsl(220, 25%, 12%);
/* Custom dark mode shadows */
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .article-card {
border-color: hsl(var(--primary-hue), 30%, 25%);
}Add responsive breakpoint-specific styles:
/* Mobile */
@media (max-width: 767px) {
.article-card__title {
font-size: 1.25rem;
}
}
/* Tablet */
@media (min-width: 768px) and (max-width: 1023px) {
.article-card {
padding: 1.5rem;
}
}
/* Desktop */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
}
}To customize templates without modifying core files:
- Copy the template from
plugins/themes/premium/templates/to your custom location - Modify as needed
- Register custom template path in
PremiumThemePlugin.php
Example: Custom Article Template
-
Copy
templates/frontend/pages/article.tpltotemplates/custom/article.tpl -
Modify
PremiumThemePlugin.php:
public function init(): void
{
parent::init();
// Register custom template directory
$this->addTemplateDirectory('custom');
}{* templates/custom/article.tpl *}
{include file="frontend/components/header.tpl"}
<div class="custom-article-layout">
{* Your custom layout *}
<div class="article-sidebar">
{* Custom sidebar content *}
</div>
<div class="article-main">
{* Article content *}
<h1>{$article->getLocalizedTitle()|escape}</h1>
{* ... *}
</div>
</div>
{include file="frontend/components/footer.tpl"}{* Add custom section to indexJournal.tpl *}
<section class="featured-authors section-padding">
<div class="container">
<h2>Featured Authors</h2>
{* Custom author showcase *}
</div>
</section>Add scripts through the admin interface:
// Add Google Analytics
(function() {
var ga = document.createElement('script');
ga.src = 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID';
document.head.appendChild(ga);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
})();Create a new JavaScript module:
// js/src/modules/customFeature.js
export class CustomFeature {
init() {
console.log('Custom feature initialized');
this.setupEventListeners();
}
setupEventListeners() {
document.querySelectorAll('.custom-trigger').forEach(el => {
el.addEventListener('click', () => this.handleClick());
});
}
handleClick() {
// Custom functionality
}
}Register in main.js:
import { CustomFeature } from './modules/customFeature.js';
// In bootstrap()
this.modules.set('customFeature', new CustomFeature());Access and extend theme modules:
// Wait for theme to initialize
document.addEventListener('DOMContentLoaded', () => {
const themeApp = window.PremiumTheme;
// Get navigation module
const nav = themeApp.getModule('navigation');
// Add custom behavior
document.addEventListener('scroll', () => {
// Custom scroll behavior
});
});Add new theme options:
// In PremiumThemePlugin.php init()
$this->addOption('customFeature', 'FieldOptions', [
'type' => 'radio',
'label' => 'plugins.themes.premium.option.customFeature.label',
'default' => 'enabled',
'options' => [
'enabled' => 'plugins.themes.premium.option.customFeature.enabled',
'disabled' => 'plugins.themes.premium.option.customFeature.disabled',
]
]);Pass custom data to templates:
public function addCustomStyles($hookName, $args): bool
{
$templateMgr = $args[0];
// Get your custom data
$customData = $this->getCustomData();
// Pass to template
$templateMgr->assign([
'customVariable' => $customData,
]);
return false;
}Use in templates:
{if $customVariable}
<div class="custom-content">
{$customVariable|escape}
</div>
{/if}Add your own preset to ColorSchemeManager.php:
public function getPresetSchemes(): array
{
return [
// Existing schemes...
'custom' => ['hue' => 180, 'name' => 'Custom Teal'],
'brand' => ['hue' => 275, 'name' => 'Brand Purple'],
];
}Register in PremiumThemePlugin.php:
$this->addOption('colorScheme', 'FieldOptions', [
'options' => [
// Existing...
'custom' => 'plugins.themes.premium.option.colorScheme.custom',
'brand' => 'plugins.themes.premium.option.colorScheme.brand',
]
]);Generate colors programmatically:
$colorManager = new ColorSchemeManager();
$scheme = $colorManager->generateScheme('#00BFA5'); // Teal
// Returns array with all shades
// ['primary-50', 'primary-100', ..., 'primary-900']- Define layout in CSS:
/* styles/src/layouts/custom.css */
.layout-custom {
display: grid;
grid-template-columns: 200px 1fr 300px;
gap: 2rem;
}
@media (max-width: 1023px) {
.layout-custom {
grid-template-columns: 1fr;
}
}- Add option to theme:
$this->addOption('layout', 'FieldOptions', [
'options' => [
// Existing...
'custom' => 'plugins.themes.premium.option.layout.custom',
]
]);- Use in templates:
<div class="layout-{$premiumLayout|default:'custom'}">
{* Template content *}
</div>{* templates/frontend/components/custom-block.tpl *}
<div class="sidebar-block custom-block">
<h3 class="sidebar-block__title">
{translate key="plugins.themes.premium.customBlock.title"}
</h3>
<div class="custom-block__content">
{* Your custom content *}
<ul>
<li><a href="#">Custom Link 1</a></li>
<li><a href="#">Custom Link 2</a></li>
</ul>
</div>
</div>Include in sidebar:
{* In indexJournal.tpl sidebar *}
<aside class="sidebar">
{include file="frontend/components/custom-block.tpl"}
</aside>- Use PurgeCSS - Already configured, but you can add safelist:
// tailwind.config.js
safelist: [
'custom-class',
'another-class',
{
pattern: /^custom-/,
},
],- Critical CSS - Inline critical styles:
// In template head
<style>
<?php echo file_get_contents('path/to/critical.css'); ?>
</style>- Lazy load modules:
// Load module only when needed
document.querySelector('.trigger').addEventListener('click', async () => {
const { HeavyModule } = await import('./modules/heavyModule.js');
const module = new HeavyModule();
module.init();
});- Debounce expensive operations:
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const expensiveOp = debounce(() => {
// Expensive operation
}, 300);
window.addEventListener('scroll', expensiveOp);- Always use custom CSS field or template overrides
- Create child themes if extensive modifications needed
- Document all customizations
- Keep ARIA labels
- Maintain keyboard navigation
- Test with screen readers
- Check color contrast (4.5:1 minimum)
- Test on mobile, tablet, desktop
- Check different browsers
- Validate dark mode
- Test RTL languages if applicable
- Minimize custom CSS/JS
- Use CSS custom properties over hardcoded values
- Lazy load images and heavy scripts
- Monitor Lighthouse scores
- Test customizations after theme updates
- Keep custom code separate from core
- Document custom code thoroughly
- Version control your customizations
- Check CSS specificity
- Clear browser cache (hard refresh)
- Verify custom CSS is loaded (inspect element)
- Check for syntax errors in browser console
- Open browser console (F12)
- Check for errors in red
- Verify module imports
- Test in isolation
- Clear OJS cache (
cache/directory) - Check template syntax
- Verify file permissions
- Check OJS error logs
- Review Configuration Guide
- Check Development Guide
- Visit OJS Community Forum
- GitHub Issues for bugs
- Professional support: support@example.com
Here's a complete example of adding a "Reading Time" feature to articles:
.reading-time {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--color-primary-50);
color: var(--color-primary-700);
border-radius: var(--radius-md);
font-size: 0.875rem;
}// js/src/modules/readingTime.js
export class ReadingTime {
init() {
this.calculateReadingTime();
}
calculateReadingTime() {
const article = document.querySelector('.article-content');
if (!article) return;
const text = article.textContent;
const wordCount = text.split(/\s+/).length;
const readingTime = Math.ceil(wordCount / 200); // 200 words per minute
this.displayReadingTime(readingTime);
}
displayReadingTime(minutes) {
const element = document.createElement('div');
element.className = 'reading-time';
element.innerHTML = `
<svg>...</svg>
<span>${minutes} min read</span>
`;
const target = document.querySelector('.article-header');
target.appendChild(element);
}
}// In main.js
import { ReadingTime } from './modules/readingTime.js';
this.modules.set('readingTime', new ReadingTime());npm run buildDone! Reading time now appears on all articles.