✨ Bootstrap 5
MODX 3: Bootstrap 5 mit Bootswatch-Themes integrieren
Diese Anleitung nutzt Bootstrap 5.3.8 mit Themes von Bootswatch als Beispiel und zeigt, wie du Bootstrap lokal in einem Ordner
bs5ablegst, die Bootswatch-Themes vorbereitest, überflüssige Dateien entfernst und externe Google-Font-Imports aus den Bootswatch-bootstrap.min.cssbereinigst. Seit Bootstrap 5.3 wird außerdem ein integrierter Darkmodus überdata-bs-themeunterstützt, daher funktioniert das Vorgehen auch bei anderen Themes, die auf einer ähnlichen Struktur basieren.
Link zu den gängigen Vorlagen für Websites und Apps. Sie basieren auf vorhandenen Komponenten und Utilities und können mit eigenem CSS erweitert werden.
https://getbootstrap.com/docs/5.3/examples/
Bootstrap 5 lokal in MODX nutzen (ohne externe Quellen)
Bootstrap soll vollständig lokal in MODX eingebunden werden, damit keine externen Ressourcen wie CDNs oder Google-Fonts geladen werden. Dieser Abschnitt beschreibt, wie du Bootstrap 5 herunterlädst und in den Ordner assets/bs5 kopierst.
📥 Bootstrap ZIP herunterladen
- Öffne die offizielle Bootstrap Release-Seite:
👉 https://github.com/twbs/bootstrap/releases/latest - Lade das ZIP herunter (z.B. bootstrap-5.x.x-dist.zip).
- Entpacke das ZIP auf deinem Linux Manjaro Desktop.
📁 Benötigte Dateien aus dem Bootstrap-Paket
Im entpackten Verzeichnis findest du:
bootstrap-5.x.x/dist/css/bootstrap.min.css
bootstrap-5.x.x/dist/js/bootstrap.bundle.min.js
Diese beiden Dateien reichen vollständig aus:
- bootstrap.min.css → alle Bootstrap 5 Styles lokal
- bootstrap.bundle.min.js → Bootstrap JS + Popper lokal
Popper ist bereits im Bundle enthalten.
📂 Bootstrap lokal in dein MODX-Projekt kopieren
Erstelle in MODX folgenden Ordner (Empfehlung):
assets/bs5/
Kopiere die beiden Dateien hinein:
assets/bs5/bootstrap.min.css
assets/bs5/bootstrap.bundle.min.js
Damit ist Bootstrap vollständig lokal eingebunden.
Vorbereitung in MODX
Führe in MODX folgende Schritte durch:
-
Lege eine neue Kategorie an, z.B.:
- BS5
-
Erstelle ein neues Template, z.B.:
- tmpBS5
-
Setze dieses Template in den Systemeinstellungen als
default_template -
Lege folgende Chunks an (für pdoMenu):
- menuBSOuter
- menuBSLink
- menuBSParent
- tmpBS_meinStyle
Diese werden später für die Navigation benötigt.
-
Lege einen eigenen Chunk für den Footer an (das macht die Struktur übersichtlicher):
- bsFooter
-
Optional kannst du eine Template Variable (TV) anlegen, um den Seitentitel ein- oder ausblenden zu können:
- tvPagetitle
- Optionsschaltflächen (Radio-Buttons)
- Radio-Button-Optionen:
ja||nein- Standardoption:
ja - Leere Eingabe erlauben:
Ja - Spalten:
1
- Standardoption:
- Anschließend die TV dem Template zuweisen (Reiter Template-Zugriff)
- tvPagetitle
TVs lassen sich sehr gut um viele Funktionen erweitern.
MODX Template tmpBS5 mit pdoMenu
Im folgenden Beispiel wird das Template tmpBS5 so aufgebaut, dass:
- Bootstrap 5 wird vollständig lokal eingebunden
- Ein Bootswatch-Theme (z.B. minty) wird lokal zugeschaltet; die Theme-Zeile ist bereits aktiv
- Die Navigation wird mit
pdoMenuerzeugt
Template-Inhalt (tmpBS5)
Optional vorbereitet für Darkmodus
<!doctype html>
<html lang="de" data-bs-theme="light">
<head>
<meta charset="utf-8">
<title>[[*pagetitle]] - [[++site_name]]</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="[[++site_url]]">
<link rel="icon" type="image/png" href="assets/modx/content/images/icon_48x48.png">
<!-- Bootstrap Grund-Styles (lokal) -->
<link rel="stylesheet" href="[[++assets_url]]bs5/bootstrap.min.css">
<!-- Bootswatch-Theme (lokal, z.B. spacelab) -->
<link rel="stylesheet" href="[[++assets_url]]bs5/spacelab/bootstrap.min.css">
<!-- Bootstrap Icons (optional, für .bi-* Icons) -->
<link rel="stylesheet" href="[[++assets_url]]bs5/bootstrap-icons/bootstrap-icons.min.css">
<!-- Eigene Styles (optional) -->
<style>[[$tmpBS_meinStyle]]</style>
<style>[[$tmpBS_DarkFixes]]</style>
</head>
<body>
<header class="mb-4">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="[[~1? &scheme=`abs`]]">[[++site_name]]</a>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#mainMenu"
aria-controls="mainMenu"
aria-expanded="false"
aria-label="Navigation umschalten">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainMenu">
[[!pdoMenu?
&parents=`0`
&level=`2`
&scheme=`abs`
&tpl=`menuBSLink`
&tplParentRow=`menuBSParent`
&tplOuter=`menuBSOuter`
&tplInner=`menuBSInner`
&hereClass=`active`
&firstClass=`first`
&lastClass=`last`
]]
<!-- Optional vorbereitet für Darkmodus -->
<button class="btn btn-sm btn-outline-light ms-lg-3 mt-3 mt-lg-0"
type="button"
id="themeToggle"
title="Farbschema umschalten: Auto → Hell → Dunkel"
aria-label="Farbschema umschalten">
<i class="bi bi-moon-stars" id="themeIcon"></i>
</button>
</div>
</div>
</nav>
</header>
<main class="container mb-5">
<!-- Optional mit einer TV den Seitentitel ein- oder ausblenden -->
[[*tvPagetitle:is=`ja`:then=`
<h1 class="mb-3">[[*longtitle:default=`[[*pagetitle]]`]]</h1>
`:else=``]]
[[!*id:isnot=`1`:then=`
<nav aria-label="breadcrumb" class="mb-3">
<div class="d-flex flex-wrap gap-2">
<a href="[[~1? &scheme=`abs`]]" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-house"></i> Start
</a>
[[!pdoCrumbs?
&scheme=`abs`
&showHome=`0`
&showAtHome=`0`
&tplWrapper=`@INLINE [[+output]]`
&tpl=`@INLINE <a href="[[+link]]" class="btn btn-sm btn-outline-secondary">[[+menutitle]]</a>`
&tplCurrent=`@INLINE <span class="btn btn-sm btn-secondary disabled">[[+menutitle]]</span>`
]]
</div>
</nav>
`:else=``]]
[[*content]]
<div style="clear: both"></div>
</main>
<footer class="bg-light border-top py-3">
<div class="container">
<p class="small text-muted mb-0">
© [[!copyrightYear? &start=`2004`]] [[++site_name]]
</p>
</div>
</footer>
<!-- JS Darkmodus -->
[[$modxBS_JS_ToggleSave]]
<!-- Bootstrap JS Bundle (lokal, inkl. Popper) -->
<script src="[[++assets_url]]bs5/bootstrap.bundle.min.js"></script>
</body>
</html>
JS-Inhalt (modxBS_JS_ToggleSave)
<script>
(function () {
var storageKey = 'pms-color-mode';
var toggleBtn = document.getElementById('themeToggle');
var icon = document.getElementById('themeIcon');
var MODES = ['auto', 'light', 'dark'];
function getStoredMode() {
try {
var m = localStorage.getItem(storageKey);
return MODES.indexOf(m) !== -1 ? m : null;
} catch (e) {
return null;
}
}
function storeMode(mode) {
try {
localStorage.setItem(storageKey, mode);
} catch (e) {
// z.B. Private Mode
}
}
function getSystemMode() {
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function applyTheme(mode) {
var html = document.documentElement;
var effective = mode === 'auto' ? getSystemMode() : mode;
html.setAttribute('data-bs-theme', effective);
if (!icon) return;
// Icon je nach aktuellem Modus/Zustand ändern:
icon.classList.remove('bi-moon-stars', 'bi-sun', 'bi-circle-half');
if (mode === 'auto') {
// Systemmodus aktiv
icon.classList.add('bi-circle-half');
} else if (effective === 'dark') {
// aktuell dunkel (manuell gewählt)
icon.classList.add('bi-moon-stars');
} else {
// aktuell hell (manuell gewählt)
icon.classList.add('bi-sun');
}
// Tooltip an aktuellen Zustand anpassen
if (toggleBtn) {
var title;
if (mode === 'auto') {
title = 'Aktuelles Farbschema: System (automatisch)';
} else if (effective === 'dark') {
title = 'Aktuelles Farbschema: dunkel';
} else {
title = 'Aktuelles Farbschema: hell';
}
toggleBtn.setAttribute('title', title);
toggleBtn.setAttribute('aria-label', title);
}
}
function initTheme() {
var mode = getStoredMode();
if (!mode) {
mode = 'auto'; // Standard: Systemmodus
storeMode(mode);
}
applyTheme(mode);
// Auf Systemwechsel reagieren, wenn auto gewählt ist
if (window.matchMedia) {
var mq = window.matchMedia('(prefers-color-scheme: dark)');
if (mq.addEventListener) {
mq.addEventListener('change', function () {
if (getStoredMode() === 'auto') {
applyTheme('auto');
}
});
} else if (mq.addListener) { // ältere Browser
mq.addListener(function () {
if (getStoredMode() === 'auto') {
applyTheme('auto');
}
});
}
}
}
initTheme();
if (toggleBtn) {
toggleBtn.addEventListener('click', function () {
var current = getStoredMode() || 'auto';
var idx = MODES.indexOf(current);
var next = MODES[(idx + 1) % MODES.length]; // auto -> light -> dark -> auto ...
storeMode(next);
applyTheme(next);
});
}
})();
</script>
🧩 Chunks für pdoMenu
Chunk: menuBSInner
[[+wrapper]]
Chunk: menuBSLink
<li class="nav-item">
<a class="nav-link [[+class]]" href="[[+link]]">[[+menutitle]]</a>
</li>
Chunk: menuBSOuter
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
[[+wrapper]]
</ul>
Chunk: menuBSParent
<li class="nav-item dropdown [[+class]]">
<a class="nav-link dropdown-toggle" href="[[+link]]" data-bs-toggle="dropdown" aria-expanded="false">
[[+menutitle]]
</a>
<ul class="dropdown-menu">
[[+wrapper]]
</ul>
</li>
Chunk: bsFooter
<footer class="bg-light border-top py-3">
<div class="container">
<p class="small text-muted mb-0">
© [[!copyrightYear? &start=`2014`]] [[++site_name]]
|
<i class="bi bi-clock-history"></i>
Lastupdate: [[!SiteLastUpdated:strtotime:date=`%Y-%m-%d`? &context=`web`]]
</p>
<p class="small text-muted mb-0">
<i class="bi bi-shield-lock"></i>
<a href="[[~42]]" target="_self">Impressum</a>
|
<i class="bi bi-exclamation-circle"></i>
<a href="[[~12]]" target="_self">Datenschutz</a>
|
<i class="bi bi-envelope"></i>
<a href="[[~55]]" target="_self">Kontakt</a>
</p>
</div>
</footer>
(Die Bootstrap-Icons intallieren wir später)
Chunk: tmpBS_meinStyle
/* Blogkarten: wenn nur eine Spalte vorhanden ist (kein Bild),
soll die Textspalte die volle Breite nutzen */
.modx-blog-card .row.g-0 > .col-md-9:only-child {
flex: 0 0 100%;
max-width: 100%;
}
/* Dropdown-Hintergrund */
.navbar .dropdown-menu {
background-color: #ffffff;
border: 1px solid #ddd;
border-radius: 6px;
padding: 0.25rem 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Einzelne Einträge etwas luftiger */
.navbar .dropdown-menu .nav-link {
color: #212529;
padding: 0.4rem 0.75rem;
border-radius: 4px;
white-space: nowrap;
}
/* Hover */
.navbar .dropdown-menu .nav-link:hover,
.navbar .dropdown-menu .nav-link:focus {
background-color: #e9ecef;
color: #212529;
}
/* Spacelab-Theme: Stil für blockquote ohne Klassen */
blockquote {
margin: 0 0 1.5rem;
padding: 0.75rem 1.25rem;
border-left: 4px solid #b3bbc5; /* dezente silber-blau-graue Linie */
color: #495057; /* Textfarbe passend zum Theme */
background-color: #f8f9fa; /* heller Hintergrund zum Absetzen */
font-style: italic;
line-height: 1.4;
}
/* Bilder im RTE immer sauber einbinden */
main img {
max-width: 100%;
height: auto;
display: block; /* verhindert hässliche Umbrüche */
margin: 0.5rem 0 1.25rem; /* schöner Abstand im Text */
border-radius: 0.25rem; /* leichte Rundung passend zu Spacelab */
box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}
/* Bilder mit Text links/rechts fließen lassen – TinyMCE erzeugt <img style="float:left"> */
main img[style*="float: left"],
main img[style*="float:left"] {
float: left;
margin: 0.25rem 1rem 1rem 0;
max-width: 50%;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}
main img[style*="float: right"],
main img[style*="float:right"] {
float: right;
margin: 0.25rem 0 1rem 1rem;
max-width: 50%;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}
/* Listen im Content schöner neben Float-Bildern darstellen */
main ul:not(.list-group) {
list-style-position: inside;
padding-left: 0;
margin-left: 1.1rem;
}
/* Nach Float sauber resetten */
main::after {
content: "";
display: block;
clear: both;
}
.download.card.file-list-card {
max-width: 520px;
margin: 2rem 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.10);
}
.file-list-card .card-header {
background-color: #f8f9fa;
font-weight: 600;
font-size: 0.95rem;
}
.file-list-card .list-group-item {
padding: 0.35rem 0.75rem;
}
.file-list-card .file-link {
display: inline-flex;
align-items: center;
}
.file-list-card .file-link:hover {
text-decoration: underline;
}
.file-list-card .badge {
font-size: 0.75rem;
font-weight: 400;
}
/* bs5GalleryModal – Bild, Infobar, Buttons */
.bs5-gallery-modal .modal-body {
position: relative;
padding: 0.5rem 2.5rem 3.5rem; /* links/rechts Platz für Pfeile, unten für Zoom */
overflow: auto;
}
/* Bild: nicht zu hoch + Luft nach unten */
.bs5-gallery-modal img {
max-height: 60vh; /* genug Platz für Caption + Buttons */
margin-bottom: 1rem;
transform-origin: center center;
transition: transform 0.2s ease;
position: relative;
z-index: 1;
}
/* Infobar (Name · Größe · Datum) gut lesbar */
.bs5-gallery-modal .gallery-infobar {
color: #f8f9fa;
opacity: 0.9;
margin-bottom: 0.5rem;
}
/* Alle Buttons im Modal über das Bild legen */
.bs5-gallery-modal .modal-body .btn {
z-index: 2;
}
/* Pfeil-Buttons als runde „Bubbles“ */
.bs5-gallery-modal .btn-outline-light {
border-radius: 50%;
width: 2.2rem;
height: 2.2rem;
padding: 0;
line-height: 2.2rem;
}
/* Galerie-Modal darf fast die ganze Breite nutzen */
.bs5-gallery-modal .modal-dialog {
max-width: 90vw;
}
/* Ab Desktop ruhig höher gehen */
@media (min-width: 992px) {
.bs5-gallery-modal img {
max-height: 75vh;
}
}
/* FULLCalendar */
.fc .fc-event-title {
white-space: normal !important;
overflow-wrap: anywhere;
line-height: 1.2;
}
@media (max-width: 600px) {
/* Buttons in der FullCalendar-Toolbar verkleinern */
.fc .fc-toolbar-chunk .btn {
padding: 2px 6px;
font-size: 0.75rem;
line-height: 1.1;
}
/* Optional: Abstand zwischen Buttons leicht reduzieren */
.fc .fc-toolbar-chunk .btn-group .btn {
margin-right: 2px;
}
}
/* Standard: Events erstmal "neutral" lassen */
.fc a.fc-event {
cursor: default;
text-decoration: none;
}
/* Nur Events mit href als Link behandeln */
.fc a.fc-event[href] {
cursor: pointer;
transition: filter 0.15s ease, opacity 0.15s ease;
}
/* Hover-Effekt NUR bei Events mit href */
.fc a.fc-event[href]:hover {
filter: brightness(1.15);
opacity: 0.9;
}
/* Titel nur bei klickbaren Events unterstreichen */
.fc a.fc-event[href] .fc-event-title {
text-decoration: underline;
}
/* Wetter-Events im Kalender */
.fc-event-weather {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
.fc-weather-event {
display: flex;
flex-direction: column;
align-items: flex-start; /* links ausrichten */
justify-content: flex-start;
padding: 0.2rem 0.2rem;
}
.fc-weather-icon {
max-width: 48px; /* evtl. anpassen */
max-height: 48px;
display: block;
margin-bottom: 2px;
}
.fc-weather-temp {
font-size: 0.8rem;
line-height: 1.1;
white-space: nowrap;
}
Chunk: tmpBS_DarkFixes
Darkmodus
/* ==========================================
Bootswatch / Bootstrap 5.3 Darkmode Fixes
Universell für alle Themes
========================================== */
/* Dropdown-Menüs */
[data-bs-theme="dark"] .navbar .dropdown-menu {
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
}
[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link {
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link:hover,
[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link:focus {
background-color: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
/* Card-Header (z.B. Downloads, Module usw.) */
[data-bs-theme="dark"] .card-header {
background-color: var(--bs-secondary-bg) !important;
color: var(--bs-body-color) !important;
border-bottom-color: var(--bs-border-color);
}
/* List-Groups (z.B. Download-Listen) */
[data-bs-theme="dark"] .list-group-item {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
/* Footer */
[data-bs-theme="dark"] footer,
[data-bs-theme="dark"] .pms-footer {
background-color: var(--bs-body-bg) !important;
color: var(--bs-secondary-color) !important;
border-top: 1px solid var(--bs-border-color);
}
[data-bs-theme="dark"] footer a {
color: var(--bs-link-color);
}
[data-bs-theme="dark"] footer a:hover {
color: var(--bs-link-hover-color);
}
/* Blockquotes */
[data-bs-theme="dark"] blockquote {
background-color: rgba(255,255,255,0.08);
color: var(--bs-body-color);
border-left-color: var(--bs-border-color);
}
/* Tabellen */
[data-bs-theme="dark"] table {
color: var(--bs-body-color);
}
[data-bs-theme="dark"] table thead {
background-color: var(--bs-secondary-bg);
}
[data-bs-theme="dark"] table td,
[data-bs-theme="dark"] table th {
border-color: var(--bs-border-color);
}
/* Breadcrumb-Buttons */
[data-bs-theme="dark"] .btn-outline-secondary {
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .btn-outline-secondary:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
/* Bilder leicht abdunkeln (optional)
[data-bs-theme="dark"] img {
box-shadow: 0 4px 16px rgba(0,0,0,0.7);
}
*/
/* FullCalendar Wetter-Temperaturen */
[data-bs-theme="dark"] .fc-weather-temp {
color: #f8f9fa;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
Snippet: copyrightYear
<?php
# [[!copyrightYear]]
# [[!copyrightYear? &start=`2020`]]
$firstYear = !empty($start) ? $start : strftime("%Y");
$currentYear = strftime("%Y");
if ($firstYear == $currentYear) {
$output = $currentYear;
} else {
$output = $firstYear.' - '.$currentYear;
}
return $output;
In einem zweiten Browserfenster die Frontpage prüfen. Eventuell ist ein Refresh (F5) notwendig.
🧩 Reihenfolge ist wichtig
Richtig:
- Bootstrap CSS
- Bootswatch Theme CSS
- Eigene Styles CSS
- Bootstrap JS (bundle)
Nur so überschreibt Bootswatch korrekt die Bootstrap-Styles.
✅ Ergebnis
- Bootstrap läuft 100% lokal
- Keine externen Skripte oder Fonts
- DSGVO-konform
- tmpBS5 ist vorbereitet und als Standard gesetzt
- Navigation wird über pdoMenu + Chunks korrekt im Bootstrap-Stil ausgegeben
- Saubere, erweiterbare Basis für ein MODX 3 Projekt