Kalender ohne Flatpickr – vollständig selbst gebaut
Flatpickr bringt Kilobytes jQuery-Erbe und eine externe Abhängigkeit mit. Ein eigener Alpine.js-Datepicker entsteht aus weniger als 80 Zeilen reaktivem JavaScript – mit Monatsnavigation, Keyboard-Support, Min/Max-Grenzen und vollständiger ARIA-Semantik, ohne eine einzige externe Abhängigkeit.
Inhaltsverzeichnis
- 1. Warum keinen fertigen Datepicker nutzen?
- 2. Grundstruktur: x-data mit Datums-State
- 3. Kalenderraster dynamisch berechnen
- 4. Monats- und Jahresnavigation
- 5. Datumsauswahl und Eingabefeld synchronisieren
- 6. Min- und Max-Grenzen erzwingen
- 7. Keyboard-Navigation mit @keydown
- 8. ARIA-Semantik für Barrierefreiheit
- 9. Alpine-Datepicker vs. Flatpickr im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum keinen fertigen Datepicker nutzen?
Die erste Frage, die Entwickler stellen, wenn sie einen Datepicker selbst bauen sollen: Warum nicht einfach Flatpickr oder Pikaday einbinden? Die Antwort liegt in der Gewichtsrechnung. Flatpickr bringt unkomprimiert rund 40 KB JavaScript mit, hat seinen eigenen Zustandsmechanismus, der unabhängig von Alpine.js agiert, und erfordert sorgfältige Integration über x-init-Hooks, um reaktiv auf Alpine-State-Änderungen zu reagieren. Im Hyvä-Themes-Kontext, wo bewusst auf jQuery, Knockout.js und externe UI-Bibliotheken verzichtet wird, ist das ein Widerspruch zur Architekturentscheidung des Projekts.
Ein selbst gebauter Alpine.js-Datepicker hat keinen eigenen State-Manager – er ist Alpine-State. Das bedeutet: kein manuelles Synchronisieren, kein Event-Bridging, keine Initialisierungslogik, die beim Server-Side-Rendering oder bei dynamisch nachgeladenen Formularen scheitert. Die JavaScript-Datums-API ist seit ES2015 ausreichend mächtig, um ein vollständiges Kalenderraster zu berechnen. Was nach komplexer Aufgabe klingt, löst sich in der Praxis in etwa 80 Zeilen reaktivem Code auf.
Ein weiterer Aspekt: Kontrolle über das Markup. Flatpickr rendert sein DOM selbst und bietet CSS-Klassen zur Anpassung. Ein eigener Datepicker nutzt direkt Tailwind-Klassen, passt sich ins Design-System ein, ohne Konflikte zu riskieren, und kann beliebig um projektspezifische Features erweitert werden – etwa die Integration mit einem Hyvä-Formular-Validator oder die Anbindung an einen Magento-REST-Endpunkt für gesperrte Lieferdaten.
2. Grundstruktur: x-data mit Datums-State
Der State eines Datepickers ist überschaubar: das aktuell angezeigte Jahr, der aktuell angezeigte Monat, das ausgewählte Datum und ein Flag, ob der Kalender sichtbar ist. Alle vier Werte leben in einem einzigen x-data-Objekt. Dazu kommen Hilfsmethoden, die aus diesen Werten das Kalenderraster berechnen. Alpine.js hält alles reaktiv – ändert sich der Monat, rechnet der Getter das Raster sofort neu.
Die Entscheidung, ob man die Logik direkt im x-data-Inline-Objekt oder in einer registrierten Alpine-Komponente via Alpine.data() ablegt, hängt von der Wiederverwendbarkeit ab. Wird der Datepicker an mehreren Stellen gebraucht – etwa in einem Checkout-Formular und einem Filterpanel – lohnt sich Alpine.data('datepicker', () => ({...})) in einer separaten JS-Datei, die über Hyvä's Modul-System eingebunden wird. Für einen einmaligen Einsatz ist Inline ausreichend und hält das Template selbst-beschreibend.
// Alpine.js Datepicker — core state definition
// Register as reusable component in your Hyva JS init file
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', (options = {}) => ({
open: false,
selected: null, // Date object or null
viewYear: new Date().getFullYear(),
viewMonth: new Date().getMonth(), // 0-indexed
minDate: options.min ? new Date(options.min) : null,
maxDate: options.max ? new Date(options.max) : null,
// Computed: human-readable value for the input field
get inputValue() {
if (!this.selected) return '';
return this.selected.toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric'
});
},
// Computed: machine-readable ISO value for hidden input
get isoValue() {
if (!this.selected) return '';
return this.selected.toISOString().slice(0, 10);
},
toggle() { this.open = !this.open; },
close() { this.open = false; },
}));
});
3. Kalenderraster dynamisch berechnen
Das Kalenderraster ist ein Array von Wochenobjekten, jedes bestehend aus sieben Tages-Objekten. Jeder Tag trägt die Information, ob er zum aktuellen Monat gehört, ob er ausgewählt ist, ob er deaktiviert ist und welches Date-Objekt er repräsentiert. Alpine.js berechnet dieses Array in einem Getter neu, sobald sich viewYear oder viewMonth ändern – ohne manuellen Trigger.
Der Algorithmus: Ermittle den ersten Tag des Monats und leite daraus den Wochenstart ab. Fülle Tage aus dem Vormonat auf, um die erste Woche zu vervollständigen. Füge dann alle Tage des aktuellen Monats hinzu. Fülle die letzte Woche mit Tagen aus dem Folgemonat. Teile das flache Array in Wochen à sieben Elemente auf. Das Ergebnis ist ein zweidimensionales Array, das direkt mit x-for über Wochen und innerhalb davon über Tage iteriert werden kann.
// Calendar grid computation — add this inside Alpine.data('datepicker', ...)
get weeks() {
const year = this.viewYear;
const month = this.viewMonth;
const firstDay = new Date(year, month, 1);
// Monday-first: Sunday (0) → position 6, Mon (1) → 0, etc.
const startOffset = (firstDay.getDay() + 6) % 7;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrevMonth = new Date(year, month, 0).getDate();
const days = [];
// Fill days from previous month
for (let i = startOffset - 1; i >= 0; i--) {
days.push(this.makeDay(year, month - 1, daysInPrevMonth - i, false));
}
// Fill current month
for (let d = 1; d <= daysInMonth; d++) {
days.push(this.makeDay(year, month, d, true));
}
// Fill next month to complete last week
const remaining = (7 - (days.length % 7)) % 7;
for (let d = 1; d <= remaining; d++) {
days.push(this.makeDay(year, month + 1, d, false));
}
// Split into weeks
const weeks = [];
for (let i = 0; i < days.length; i += 7) {
weeks.push(days.slice(i, i + 7));
}
return weeks;
},
makeDay(year, month, day, currentMonth) {
const date = new Date(year, month, day);
const isSelected = this.selected &&
date.toDateString() === this.selected.toDateString();
const isDisabled = (this.minDate && date < this.minDate) ||
(this.maxDate && date > this.maxDate);
const isToday = date.toDateString() === new Date().toDateString();
return { date, day, currentMonth, isSelected, isDisabled, isToday };
},
4. Monats- und Jahresnavigation
Die Navigation zwischen Monaten und Jahren ist die einfachste Methode des Datepickers. Ein Klick auf den Zurück-Pfeil dekrementiert viewMonth um 1 – ist der Wert dann -1, wird er auf 11 gesetzt und viewYear um 1 dekrementiert. Alpine.js propagiert die Änderung sofort in den weeks-Getter, der das neue Raster berechnet. Durch die Reaktivität von Alpine entsteht die Animation des Kalenders ohne eine einzige DOM-Manipulation.
Für die Jahresnavigation kann man entweder Pfeile um jeweils ein Jahr oder ein Dropdown mit einem Jahresbereich anbieten. Das Dropdown-Muster ist ergonomischer bei weit entfernten Daten – etwa bei einem Geburtsdatum-Feld. Es entsteht mit einem <select>-Element, dessen value per x-model an viewYear gebunden ist und dessen Optionen per x-for aus einem berechneten Jahresbereich generiert werden.
5. Datumsauswahl und Eingabefeld synchronisieren
Ein Klick auf einen Tag-Button setzt this.selected auf das Date-Objekt des Tages und schließt das Kalender-Dropdown. Das Eingabefeld zeigt über den inputValue-Getter sofort das formatierte Datum an. Der versteckte <input type="hidden"> mit dem ISO-Wert sorgt dafür, dass beim Form-Submit der maschinenlesbare Wert gesendet wird, während der Benutzer eine lokale Datumsdarstellung sieht.
Wer manuelle Texteingabe erlauben will, muss die Textfeld-Änderungen parsen und validieren. Das geschieht in einer parseInput-Methode: Sie versucht, den eingetippten String als deutsches Datum (TT.MM.JJJJ) zu interpretieren, prüft, ob das Ergebnis ein valides Datum ist, und setzt this.selected nur dann, wenn Parsing und Validierung erfolgreich waren. Ungültige Eingaben werden durch ein visuelles Fehlersignal an den Nutzer kommuniziert, ohne die Alpine-State-Konsistenz zu gefährden.
6. Min- und Max-Grenzen erzwingen
Min- und Max-Datumsangaben kommen typischerweise aus dem Magento-Backend – etwa als PHP-generierte data-min- und data-max-Attribute auf dem Container-Element. Das Alpine.data-Komponent nimmt diese Werte als options-Parameter entgegen und wandelt sie im Konstruktor in Date-Objekte um. Der makeDay-Helfer setzt das isDisabled-Flag, das Template rendert disabled Tage mit reduzierter Opazität und ohne click-Handler.
Wichtig: Die Validierung muss auch serverseitig stattfinden. Das Alpine-Frontend schützt die UX, aber die Min/Max-Logik im Browser ist kein Sicherheitsmerkmal – ein Angreifer kann den Wert des Hidden-Inputs direkt manipulieren. Im Magento-Controller muss die Datumsvalidierung wiederholt werden. Alpine übernimmt die UX-Schicht, PHP die Sicherheitsschicht.
// Full datepicker HTML template (abbreviated) — use in Magento .phtml
// Assumes Alpine.data('datepicker') is registered
// Day selection and close — methods inside Alpine.data('datepicker', ...)
selectDay(day) {
if (day.isDisabled) return;
this.selected = day.date;
// If selected date is in prev/next month, navigate there
if (!day.currentMonth) {
this.viewYear = day.date.getFullYear();
this.viewMonth = day.date.getMonth();
}
this.close();
// Dispatch to parent Alpine component or Magento form listeners
this.$dispatch('date-selected', {
iso: this.isoValue,
display: this.inputValue,
});
},
prevMonth() {
if (this.viewMonth === 0) { this.viewMonth = 11; this.viewYear--; }
else { this.viewMonth--; }
},
nextMonth() {
if (this.viewMonth === 11) { this.viewMonth = 0; this.viewYear++; }
else { this.viewMonth++; }
},
get monthLabel() {
return new Date(this.viewYear, this.viewMonth, 1)
.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
},
7. Keyboard-Navigation mit @keydown
Ein Datepicker, der nur per Maus bedienbar ist, besteht keine Barrierefreiheitsprüfung. Keyboard-Navigation bedeutet: Pfeiltasten navigieren zwischen Tagen, Enter wählt den fokussierten Tag aus, Escape schließt das Dropdown, Page Up/Page Down navigieren zwischen Monaten. Alpine.js bindet diese mit @keydown.arrow-left, @keydown.arrow-right, @keydown.enter und @keydown.escape direkt an Methoden.
Der fokussierte Tag wird im State als focusedDate gehalten. Jeder Tag-Button bekommt ein :tabindex-Binding – nur der fokussierte Tag hat tabindex="0", alle anderen tabindex="-1". Wenn der Fokus über Pfeiltasten wechselt, setzt eine $nextTick-Callback die DOM-Fokus auf den neuen Button. Dieses Muster nennt sich Roving Tabindex und ist der ARIA-Standard für Grid-Widgets wie Kalender.
8. ARIA-Semantik für Barrierefreiheit
Der Kalender-Container bekommt role="dialog" und aria-modal="true". Das Trigger-Eingabefeld bekommt aria-haspopup="dialog" und :aria-expanded="open". Jeder Tag-Button bekommt :aria-label mit dem voll ausgeschriebenen Datum auf Deutsch, :aria-selected für den ausgewählten Tag und :aria-disabled für deaktivierte Tage. Der Monatsheader bekommt aria-live="polite", damit Screenreader bei Monatsnavigation den neuen Monatsnamen ankündigen.
Focus-Trapping ist ein weiterer wichtiger Aspekt: Während der Kalender offen ist, soll der Fokus beim Tab-Navigieren innerhalb des Kalenders bleiben. Das lässt sich mit einem @keydown.tab-Handler erreichen, der prüft, ob der Fokus das letzte fokussierbare Element im Dialog verlassen würde, und ihn gegebenenfalls zurück auf das erste Element setzt. Alpine.js macht das mit this.$el.querySelectorAll('[tabindex="0"]') zugänglich.
9. Alpine-Datepicker vs. Flatpickr im Vergleich
Die Entscheidung für einen selbst gebauten Datepicker ist nicht immer die richtige. Die folgende Tabelle hilft bei der Abwägung.
| Kriterium | Flatpickr | Alpine.js selbst gebaut | Empfehlung |
|---|---|---|---|
| Bundle-Größe | ~40 KB JS + CSS | ~3 KB inline | Alpine bei Gewichtsoptimierung |
| Alpine-Integration | x-init Bridge nötig | Nativer Alpine-State | Alpine – kein Bridging |
| Styling | CSS-Klassen überschreiben | Tailwind direkt | Alpine – kein CSS-Konflikt |
| Zeitbereich-Picker | Eingebaut | Selbst erweitern | Flatpickr bei Range-Bedarf |
| Wartung | Externe Abhängigkeit | Vollständige Kontrolle | Alpine – kein Upstream-Risiko |
Flatpickr bleibt die bessere Wahl für komplexe Anforderungen wie Date-Range-Auswahl mit Hover-Preview, Zeitpicker-Kombination oder Locale-intensive Anwendungen mit RTL-Support. Für einfache Datumsauswahl in einem Hyvä-Magento-Projekt ohne externe Abhängigkeiten ist der selbst gebaute Alpine-Datepicker die sauberere Lösung.
Mironsoft
Alpine.js, Hyvä Themes und Magento 2 Frontend-Entwicklung
Alpine.js-Komponenten für euer Magento-Projekt?
Wir entwickeln performante, barrierefreie Alpine.js-Komponenten für Hyvä Themes – Datepicker, Dropdowns, Formulare und Checkout-Flows – ohne externe Abhängigkeiten, vollständig ins Tailwind-Design-System integriert.
Komponenten-Entwicklung
Datepicker, Dropdowns, Tabs und Modale als Alpine.data-Komponenten
Barrierefreiheit
ARIA-konforme Implementierung, Keyboard-Navigation und Screenreader-Tests
Hyvä-Integration
Saubere Einbindung ins Hyvä-Modul-System mit CSP-konformen Inline-Scripts
10. Zusammenfassung
Ein vollständiger Alpine.js-Datepicker ohne externe Bibliothek ist in rund 80 Zeilen JavaScript und einem überschaubaren HTML-Template realisierbar. Der State lebt nativ in Alpine, die Reaktivität ist kostenlos, und das Markup nutzt direkt Tailwind-Klassen ohne CSS-Konfliktpotenzial mit externen Bibliotheken. Die Schlüsselkonzepte sind: x-data mit Gettern für Kalenderraster und formatierte Werte, x-for für die Wochen- und Tagesiteration, @keydown für Keyboard-Navigation und ARIA-Attribute für Barrierefreiheit.
Die Entscheidung für oder gegen Flatpickr hängt von den Anforderungen ab. Range-Picker, Zeitauswahl und RTL-Support sprechen für Flatpickr. Gewicht, Alpine-Integration, Tailwind-Kompatibilität und vollständige Markup-Kontrolle sprechen für den selbst gebauten Ansatz. Im Hyvä-Themes-Kontext mit seiner expliziten Abkehr von externen UI-Bibliotheken ist der Alpine-Datepicker in den meisten Fällen die architektonisch konsistentere Lösung.
Alpine.js Datepicker — Das Wichtigste auf einen Blick
State-Design
viewYear, viewMonth, selected und open in einem x-data-Objekt. Kalenderraster als Getter – bei Monatswechsel automatisch neu berechnet.
Rasteralgorithmus
Ersten Wochentag ermitteln, Vormonats-Lücken füllen, aktuellen Monat eintragen, letzte Woche vervollständigen. In 7er-Gruppen aufteilen für x-for.
Keyboard & ARIA
Roving Tabindex für Grid-Navigation. @keydown.arrow-* für Tagesfokus, Escape zum Schließen. role="dialog", aria-selected und aria-disabled.
Min/Max-Grenzen
Options-Parameter im Alpine.data-Konstruktor. isDisabled-Flag im makeDay-Helfer. Serverseitige Validierung immer zusätzlich erforderlich.