Concept is designed with theming in mind, allowing you to create custom themes that match your brand or project requirements. This guide covers everything from simple color changes to complete theme systems.
Concept's theming system works in layers:
src/scss/
├── _variables.scss # Global variables
├── themes/
│ ├── _default.scss # Default theme
│ ├── _dark.scss # Dark theme
│ └── _custom.scss # Your custom theme
├── main.scss # Import orchestration
└── custom/
└── _theme-overrides.scss # Runtime overrides
Create a new theme file:
// src/scss/themes/_corporate.scss
// Theme name and metadata
$theme-name: "corporate";
$theme-description: "Professional corporate theme";
// Color palette
$theme-colors: (
"primary": #003366,
"secondary": #0066cc,
"success": #00a86b,
"info": #17a2b8,
"warning": #ffc107,
"danger": #dc3545,
"light": #f8f9fa,
"dark": #001f3f
);
// Extended colors
$theme-extended-colors: (
"brand": #002244,
"accent": #0088ff,
"muted": #6c757d,
"highlight": #ffeb3b
);
// Typography
$theme-fonts: (
"base": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
"heading": "'Poppins', sans-serif",
"mono": "'JetBrains Mono', monospace"
);
// Spacing
$theme-spacing: (
"tight": 0.25rem,
"normal": 1rem,
"relaxed": 1.5rem,
"loose": 2rem
);
// Component specific
$theme-components: (
"border-radius": 0.375rem,
"border-radius-sm": 0.25rem,
"border-radius-lg": 0.5rem,
"box-shadow": 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075),
"box-shadow-lg": 0 0.5rem 1rem rgba(0, 0, 0, 0.15)
);
Update your main variables file:
// src/scss/_variables.scss
// Import theme
@import "themes/corporate";
// Apply theme colors to Bootstrap
$primary: map-get($theme-colors, "primary");
$secondary: map-get($theme-colors, "secondary");
$success: map-get($theme-colors, "success");
$info: map-get($theme-colors, "info");
$warning: map-get($theme-colors, "warning");
$danger: map-get($theme-colors, "danger");
$light: map-get($theme-colors, "light");
$dark: map-get($theme-colors, "dark");
// Apply theme fonts
$font-family-base: map-get($theme-fonts, "base");
$headings-font-family: map-get($theme-fonts, "heading");
$font-family-monospace: map-get($theme-fonts, "mono");
// Apply component settings
$border-radius: map-get($theme-components, "border-radius");
$border-radius-sm: map-get($theme-components, "border-radius-sm");
$border-radius-lg: map-get($theme-components, "border-radius-lg");
$box-shadow: map-get($theme-components, "box-shadow");
$box-shadow-lg: map-get($theme-components, "box-shadow-lg");
Add theme-specific utility classes:
// src/scss/themes/_corporate-utilities.scss
// Extended color utilities
@each $name, $color in $theme-extended-colors {
.text-#{$name} {
color: $color !important;
}
.bg-#{$name} {
background-color: $color !important;
}
.border-#{$name} {
border-color: $color !important;
}
.btn-#{$name} {
@include button-variant($color, $color);
}
}
// Spacing utilities
@each $name, $space in $theme-spacing {
.spacing-#{$name} {
padding: $space;
margin: $space;
}
}
// src/scss/themes/_dark-mode.scss
:root {
// Light mode defaults
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
// Dark mode overrides
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #adb5bd;
--border-color: #495057;
--shadow-color: rgba(0, 0, 0, 0.3);
// Component overrides
--card-bg: var(--bg-secondary);
--sidebar-bg: #161616;
--header-bg: var(--bg-secondary);
}
// Apply variables
body {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.card {
background-color: var(--card-bg);
border-color: var(--border-color);
}
// src/js/theme-switcher.js
export class ThemeSwitcher {
constructor() {
this.theme = this.loadTheme() || 'light';
this.init();
}
init() {
// Apply saved theme
this.applyTheme(this.theme);
// Setup toggle button
const toggle = document.getElementById('theme-toggle');
if (toggle) {
toggle.addEventListener('click', () => this.toggleTheme());
}
// Watch for system preference changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', e => {
if (this.theme === 'auto') {
this.applySystemTheme();
}
});
}
}
toggleTheme() {
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(this.theme);
this.theme = themes[(currentIndex + 1) % themes.length];
this.applyTheme(this.theme);
this.saveTheme(this.theme);
}
applyTheme(theme) {
if (theme === 'auto') {
this.applySystemTheme();
} else {
document.documentElement.setAttribute('data-theme', theme);
}
this.updateToggleButton(theme);
}
applySystemTheme() {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
updateToggleButton(theme) {
const toggle = document.getElementById('theme-toggle');
if (toggle) {
const icon = toggle.querySelector('i');
icon.className = theme === 'dark' ? 'fa-solid fa-sun' :
theme === 'auto' ? 'fa-solid fa-circle-half-stroke' :
'fa-solid fa-moon';
}
}
saveTheme(theme) {
localStorage.setItem('concept-theme', theme);
}
loadTheme() {
return localStorage.getItem('concept-theme');
}
}
// Auto-initialize
new ThemeSwitcher();
// src/scss/themes/_minimal.scss
$theme-colors: (
"primary": #000000,
"secondary": #666666,
"success": #00c853,
"danger": #ff1744,
"light": #ffffff,
"dark": #000000
);
$theme-components: (
"border-radius": 0,
"box-shadow": none,
"border-width": 1px
);
// Minimal typography
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
$headings-font-weight: 300;
$btn-font-weight: 400;
// src/scss/themes/_material.scss
$theme-colors: (
"primary": #1976d2,
"secondary": #dc004e,
"success": #388e3c,
"info": #0288d1,
"warning": #f57c00,
"danger": #d32f2f
);
$theme-components: (
"border-radius": 4px,
"box-shadow": 0 3px 1px -2px rgba(0,0,0,.2),
0 2px 2px 0 rgba(0,0,0,.14),
0 1px 5px 0 rgba(0,0,0,.12),
"transition": all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
);
// Material typography
$font-family-base: "Roboto", sans-serif;
$headings-font-weight: 400;
// src/scss/themes/_gradient.scss
// Gradient backgrounds
$gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
$gradient-success: linear-gradient(135deg, #00c853 0%, #00e676 100%);
$gradient-danger: linear-gradient(135deg, #ff5252 0%, #ff1744 100%);
// Apply to buttons
.btn-primary {
background: $gradient-primary;
border: none;
&:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46a1 100%);
}
}
// Gradient cards
.card-gradient {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border: none;
}
// Gradient text
.text-gradient {
background: $gradient-primary;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
// src/scss/components/_sidebar-theme.scss
.sidebar {
// Use theme variables
background-color: var(--sidebar-bg, $sidebar-bg);
color: var(--sidebar-color, $sidebar-color);
// Dark sidebar variant
&.sidebar-dark {
--sidebar-bg: #1e1e1e;
--sidebar-color: #ffffff;
--sidebar-link-color: rgba(255, 255, 255, 0.8);
--sidebar-link-hover: #ffffff;
--sidebar-border: rgba(255, 255, 255, 0.1);
}
// Light sidebar variant
&.sidebar-light {
--sidebar-bg: #ffffff;
--sidebar-color: #495057;
--sidebar-link-color: #495057;
--sidebar-link-hover: #000000;
--sidebar-border: rgba(0, 0, 0, 0.1);
}
// Gradient sidebar
&.sidebar-gradient {
background: linear-gradient(180deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
}
}
// src/scss/components/_card-theme.scss
.card {
// Theme variants
&.card-minimal {
border: none;
box-shadow: none;
background-color: transparent;
}
&.card-outlined {
background-color: transparent;
border-width: 2px;
}
&.card-elevated {
border: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
}
}
&.card-glassmorphic {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
}
// src/js/theme-manager.js
export class ThemeManager {
constructor() {
this.themes = {
default: () => import('./themes/default'),
dark: () => import('./themes/dark'),
corporate: () => import('./themes/corporate'),
minimal: () => import('./themes/minimal')
};
this.currentTheme = 'default';
this.init();
}
async init() {
const savedTheme = localStorage.getItem('selected-theme') || 'default';
await this.loadTheme(savedTheme);
}
async loadTheme(themeName) {
if (!this.themes[themeName]) {
console.error(`Theme "${themeName}" not found`);
return;
}
try {
// Remove previous theme
this.removeTheme(this.currentTheme);
// Load new theme
const theme = await this.themes[themeName]();
theme.apply();
this.currentTheme = themeName;
localStorage.setItem('selected-theme', themeName);
// Emit theme change event
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: { theme: themeName }
}));
} catch (error) {
console.error('Failed to load theme:', error);
}
}
removeTheme(themeName) {
document.documentElement.classList.remove(`theme-${themeName}`);
}
getAvailableThemes() {
return Object.keys(this.themes);
}
getCurrentTheme() {
return this.currentTheme;
}
}
<!-- Theme selection dropdown -->
<div class="theme-selector">
<label for="theme-select">Choose Theme:</label>
<select id="theme-select" class="form-select">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="corporate">Corporate</option>
<option value="minimal">Minimal</option>
</select>
</div>
<!-- Theme customizer panel -->
<div class="theme-customizer">
<h5>Customize Theme</h5>
<!-- Color pickers -->
<div class="color-picker-group">
<label>Primary Color</label>
<input type="color" id="primary-color" value="#5969ff">
</div>
<!-- Font selection -->
<div class="font-selector">
<label>Font Family</label>
<select id="font-family">
<option value="Inter">Inter</option>
<option value="Roboto">Roboto</option>
<option value="Poppins">Poppins</option>
</select>
</div>
<!-- Border radius -->
<div class="radius-slider">
<label>Border Radius</label>
<input type="range" id="border-radius" min="0" max="20" value="8">
</div>
</div>
// Only load theme CSS when needed
async function loadThemeCSS(themeName) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `/themes/${themeName}.css`;
link.id = `theme-${themeName}`;
document.head.appendChild(link);
// Wait for load
return new Promise((resolve) => {
link.onload = resolve;
});
}
<!-- Preload critical theme resources -->
<link rel="preload" href="/themes/default.css" as="style">
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
Explore more customization options:
With proper theming, you can transform Concept to match any brand or design system. Start with colors and typography, then expand to complete theme systems! 🎨