Grid-Template-Columns dynamisch anpassen, Drag-Handle und Mindestbreiten mit Tailwind
Ein Split-Pane-Layout mit einem per Maus verschiebbaren Trennbalken zwischen zwei Bereichen ist aus modernen Code-Editoren wie VS Code nicht mehr wegzudenken und findet sich zunehmend auch in Web-Anwendungen mit Datei-Baum, Vorschau oder Vergleichs-Ansichten. Die Herausforderung liegt weniger im offensichtlichen Teil, dem Ziehen des Trennbalkens, sondern in den Details: dynamische Grid-Spaltenbreiten, die per JavaScript und nicht per CSS allein gesetzt werden müssen, Mindestbreiten-Constraints, damit kein Panel komplett verschwindet, und ein Cursor-Feedback, das während des Ziehens konsistent bleibt. Dieser Artikel zeigt den vollständigen Aufbau eines Split-Pane-Layouts mit Tailwind CSS am Beispiel eines Code-Editors mit Datei-Baum links und Editor-Bereich rechts.
Inhaltsverzeichnis
- 1. Was ein Split-Pane-Layout technisch besonders macht
- 2. Grid-Template-Columns dynamisch per JavaScript anpassen
- 3. Drag-Handle-Styling zwischen den Panels
- 4. Mindestbreiten-Constraints für beide Panels
- 5. Cursor-Feedback während der Drag-Interaktion
- 6. Anwendungsfall: Code-Editor mit Datei-Baum und Editor-Bereich
- 7. Responsives Verhalten auf kleinen Bildschirmen
- 8. Persistenz der Panel-Breite im lokalen Speicher
- 9. Barrierefreiheit: Tastatur-Bedienung des Drag-Handles
- 10. Zusammenfassung
- 11. FAQ
1. Was ein Split-Pane-Layout technisch besonders macht
Ein Split-Pane-Layout unterscheidet sich von den meisten anderen responsiven Layouts dadurch, dass die Breitenverteilung zwischen den Panels nicht allein von der Bildschirmgröße, sondern von einer direkten Nutzerinteraktion abhängt. Während ein normales responsives Grid seine Spaltenbreiten über feste Breakpoints und Utility-Klassen bestimmt, muss ein Split-Pane die exakte Spaltenbreite als kontinuierlichen, per Drag frei wählbaren Wert speichern und bei jedem Frame während des Ziehens neu anwenden. Das sprengt den Rahmen dessen, was reine CSS-Klassen leisten können, weshalb an dieser einen Stelle zwingend JavaScript ins Spiel kommt, während der Rest des Layouts weiterhin vollständig mit Tailwind-Utilities auskommt.
Die übliche technische Grundlage ist CSS Grid mit einer grid-template-columns-Deklaration aus zwei festen oder relativen Werten und einer schmalen Spalte dazwischen für den Drag-Handle selbst. Tailwind liefert dafür die statischen Grid-Utilities wie grid grid-cols-[...], der dynamische Anteil, also die konkrete Breite der ersten Spalte, wird während der Drag-Interaktion per Inline-Style oder CSS-Custom-Property gesetzt. Diese Kombination aus statischem Tailwind-Grundgerüst und einem einzigen dynamischen Wert ist ein Muster, das sich bei fast jeder interaktiven, aber ansonsten strukturell einfachen Komponente wiederholt.
2. Grid-Template-Columns dynamisch per JavaScript anpassen
Der zentrale Mechanismus eines Split-Pane-Layouts ist eine mousedown-Aktion auf dem Drag-Handle, die einen mousemove-Listener auf dem document registriert und bei jeder Mausbewegung die neue Spaltenbreite berechnet, meist als Differenz zwischen der aktuellen Mausposition und der linken Kante des Grid-Containers. Diese berechnete Breite wird dann als CSS-Custom-Property auf dem Grid-Container gesetzt, etwa --left-width, die wiederum in der grid-template-columns-Deklaration referenziert wird. Der Vorteil dieser Custom-Property-Indirektion gegenüber einem direkten Setzen von grid-template-columns liegt darin, dass Tailwinds Arbitrary-Value-Syntax die Custom Property einmalig referenzieren kann und danach nur noch die Property selbst per JavaScript aktualisiert werden muss.
Wichtig für die Performance ist, die Breitenberechnung während des Draggens nicht bei jedem mousemove-Event ungebremst auszuführen, sondern per requestAnimationFrame zu drosseln, damit auch bei sehr schnellen Mausbewegungen keine unnötigen, sich überholenden Layout-Neuberechnungen entstehen. Der mouseup-Listener auf dem Dokument beendet die Drag-Interaktion und entfernt den mousemove-Listener wieder, da ein dauerhaft aktiver Listener sonst unnötig Ressourcen verbraucht, obwohl gerade gar keine Drag-Interaktion stattfindet.
<div
x-data="{
leftWidth: 280,
dragging: false,
startDrag(e) {
this.dragging = true;
const startX = e.clientX;
const startWidth = this.leftWidth;
const onMove = (ev) => {
const delta = ev.clientX - startX;
this.leftWidth = Math.min(480, Math.max(180, startWidth + delta));
};
const onUp = () => {
this.dragging = false;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}"
x-bind:style="`grid-template-columns: ${leftWidth}px 4px 1fr`"
class="grid h-screen">
<!-- Datei-Baum -->
<aside class="overflow-y-auto bg-slate-50 dark:bg-slate-900">Datei-Baum</aside>
<!-- Drag-Handle -->
<div
x-on:mousedown="startDrag($event)"
x-bind:class="dragging ? 'bg-sky-500' : 'bg-slate-200 hover:bg-sky-400'"
class="cursor-col-resize transition-colors"></div>
<!-- Editor -->
<main class="overflow-auto">Editor-Bereich</main>
</div>
3. Drag-Handle-Styling zwischen den Panels
Der Drag-Handle selbst sollte im Ruhezustand schmal und dezent sein, typischerweise vier Pixel breit mit einer neutralen Grundfarbe, da er im normalen Nutzungsfluss kaum Aufmerksamkeit auf sich ziehen soll. Beim Hover wird die klickbare Fläche visuell durch eine kräftigere Farbe hervorgehoben, um dem Nutzer zu signalisieren, dass hier eine Interaktion möglich ist, bevor er überhaupt zu klicken beginnt. Damit die tatsächliche Trefferfläche großzügiger ausfällt als die sichtbaren vier Pixel, empfiehlt sich ein unsichtbarer, breiterer Hover-Bereich von etwa 8 bis 12 Pixeln, der über ein zusätzliches Padding oder ein Pseudo-Element realisiert wird, ohne dass sich dadurch die tatsächliche visuelle Breite der Trennlinie ändert.
Während einer aktiven Drag-Interaktion sollte der Handle eine noch kräftigere Farbe annehmen als im reinen Hover-Zustand, damit auch bei schneller Mausbewegung eindeutig erkennbar bleibt, dass gerade tatsächlich gezogen wird und nicht nur gehovert. Zusätzlich lohnt sich ein kleiner, vertikal zentrierter Griff-Indikator innerhalb des Handles, etwa drei kleine Punkte oder Striche, der bei längeren, hohen Panels visuell markiert, wo sich der Handle befindet, besonders wenn der Trennbalken selbst über die gesamte Bildschirmhöhe verläuft und sonst leicht übersehen werden könnte.
4. Mindestbreiten-Constraints für beide Panels
Ohne explizite Mindestbreiten-Constraints kann ein Nutzer ein Panel theoretisch bis auf null Pixel zusammenziehen, was den Inhalt unbrauchbar macht und im schlimmsten Fall dazu führt, dass der Trennbalken selbst nicht mehr greifbar ist, weil er hinter dem Rand des Containers verschwunden ist. Im Code-Beispiel weiter oben wird das über Math.min(480, Math.max(180, ...)) gelöst, das die berechnete Breite auf einen festen Bereich zwischen 180 und 480 Pixeln begrenzt, bevor sie überhaupt als neuer Wert gesetzt wird, statt die Begrenzung erst nachträglich per CSS min-width zu erzwingen.
Diese Begrenzung direkt in der JavaScript-Berechnung vorzunehmen statt nur per CSS ist wichtig, weil eine reine CSS-min-width-Begrenzung zwar das visuelle Ergebnis korrekt begrenzen würde, der interne Zustand der Komponente aber trotzdem einen Wert außerhalb des erlaubten Bereichs speichern könnte, was bei einer späteren Persistierung dieses Werts, etwa im lokalen Speicher, zu falschen gespeicherten Werten führen würde. Die Mindestbreite für das rechte Panel wird indirekt über die 1fr-Einheit in der Grid-Definition sichergestellt, die automatisch den gesamten verbleibenden Platz einnimmt, zusätzlich kann eine min-width: 0-Deklaration auf dem rechten Panel nötig sein, um Overflow-Probleme bei sehr breitem Inhalt zu vermeiden.
5. Cursor-Feedback während der Drag-Interaktion
Der Cursor über dem Drag-Handle sollte durchgehend cursor-col-resize anzeigen, um dem Nutzer schon vor dem Klicken zu signalisieren, dass hier eine horizontale Größenveränderung möglich ist. Sobald eine Drag-Interaktion aktiv ist, muss dieser Cursor-Stil jedoch auch dann bestehen bleiben, wenn sich die Maus während des Ziehens kurzzeitig außerhalb des schmalen Handle-Bereichs befindet, etwa über dem Datei-Baum oder dem Editor-Inhalt, da der Browser sonst mitten in der Interaktion fälschlich zum normalen Textcursor oder Pfeil wechseln würde.
Gelöst wird das üblicherweise, indem während der aktiven Drag-Interaktion eine Klasse wie cursor-col-resize select-none auf dem <body>-Element gesetzt wird, die den Cursor global für die gesamte Seite erzwingt und gleichzeitig die Textauswahl unterbindet. Ohne diese Unterbindung der Textauswahl würde jede schnelle Mausbewegung während des Ziehens versehentlich Text im Datei-Baum oder Editor markieren, was die Interaktion sofort unangenehm und fehleranfällig macht. Beide Klassen werden im mouseup-Handler wieder vom <body> entfernt, sobald die Drag-Interaktion beendet ist.
6. Anwendungsfall: Code-Editor mit Datei-Baum und Editor-Bereich
Im konkreten Anwendungsfall eines Code-Editors bekommt das linke Panel den Datei-Baum mit eigenem vertikalem Scroll-Verhalten über overflow-y-auto, während das rechte Panel den eigentlichen Editor-Inhalt aufnimmt, der bei mehreren gleichzeitig geöffneten Dateien meist zusätzlich eigene Tabs oberhalb des Editor-Bereichs benötigt. Beide Panels teilen sich dieselbe volle Bildschirmhöhe, meist über h-screen auf dem umgebenden Grid-Container, wobei jedes Panel unabhängig vom anderen scrollen können muss, ohne dass ein Scroll-Vorgang im Datei-Baum den Editor-Bereich beeinflusst oder umgekehrt.
Bei komplexeren Editor-Oberflächen kommt häufig noch ein drittes Panel hinzu, etwa ein Terminal- oder Konsolen-Bereich unterhalb des Editors, was das Layout von einem einfachen zweispaltigen Grid zu einem verschachtelten Grid mit sowohl horizontalem als auch vertikalem Split-Handle erweitert. Der hier vorgestellte Mechanismus mit CSS-Custom-Property und JavaScript-Drag-Handler lässt sich für diesen Fall direkt wiederverwenden, lediglich die Grid-Definition wechselt von grid-template-columns zusätzlich auf eine verschachtelte grid-template-rows-Deklaration im unteren Bereich.
7. Responsives Verhalten auf kleinen Bildschirmen
Ein per Maus ziehbares Split-Pane-Layout ergibt auf einem Touchscreen mit begrenzter Bildschirmbreite wenig Sinn, da für zwei nebeneinanderliegende Panels schlicht nicht genug horizontaler Platz vorhanden ist und ein 4-Pixel-Drag-Handle auf einem Touch-Gerät ohnehin kaum präzise treffbar wäre. Unterhalb eines definierten Breakpoints, meist md, sollte das Layout deshalb komplett auf ein anderes Muster umschalten, etwa einen ausklappbaren Datei-Baum als Overlay oder Bottom-Sheet, während der Editor-Bereich die volle Bildschirmbreite für sich allein bekommt.
Diese Umschaltung lässt sich mit Tailwind über zwei parallele Layout-Varianten realisieren, die responsiv per hidden md:grid beziehungsweise md:hidden zwischen Split-Pane und mobiler Variante wechseln, statt zu versuchen, dasselbe Grid-basierte Layout mit zusätzlicher Logik für beide Fälle passend zu machen. Der JavaScript-Drag-Zustand selbst muss auf der mobilen Variante gar nicht existieren, da dort ohnehin kein Drag-Handle gerendert wird, was die Komponente insgesamt schlanker macht, statt eine einzige, alle Fälle abdeckende Implementierung mit bedingter Deaktivierung des Draggens zu pflegen.
8. Persistenz der Panel-Breite im lokalen Speicher
Nutzer, die die Panel-Breite einmal auf eine für sie passende Größe eingestellt haben, erwarten in aller Regel, dass diese Einstellung auch nach einem Seiten-Reload erhalten bleibt, statt bei jedem Besuch erneut auf die Standardbreite zurückzuspringen. Die einfachste Lösung dafür ist, den aktuellen Breitenwert bei jedem mouseup-Ereignis, also am Ende jeder Drag-Interaktion, in den lokalen Speicher des Browsers zu schreiben, statt bei jedem einzelnen mousemove-Event, da häufiges Schreiben in den lokalen Speicher während des Draggens unnötige Performance-Kosten verursachen würde.
Beim initialen Laden der Komponente wird der gespeicherte Wert aus dem lokalen Speicher gelesen und als Startwert für die Panel-Breite verwendet, wobei zwingend derselbe Mindest- und Maximalwert-Check wie bei der Drag-Interaktion selbst angewendet werden muss. Ein gespeicherter Wert, der etwa durch eine spätere Aenderung der erlaubten Grenzen ungültig geworden ist, würde sonst zu einem unbrauchbaren Layout führen, das erst durch eine erneute Nutzerinteraktion wieder in einen gültigen Zustand gebracht werden könnte.
9. Barrierefreiheit: Tastatur-Bedienung des Drag-Handles
Ein Drag-Handle, der ausschließlich per Maus bedienbar ist, schließt Tastaturnutzer und Nutzer von Screenreadern komplett von der Größenanpassung aus, weshalb der Handle semantisch als role="separator" mit aria-orientation="vertical" ausgezeichnet werden sollte, kombiniert mit tabindex="0", damit er überhaupt per Tastatur fokussierbar wird. Sobald der Handle fokussiert ist, sollten die linke und rechte Pfeiltaste die Panel-Breite in festen Schritten von etwa 20 bis 40 Pixeln verändern, damit auch ohne Maus eine vollständige Bedienung der Komponente möglich ist.
Zusätzlich zu den Pfeiltasten gehören zu einem korrekt implementierten role="separator" auch die Attribute aria-valuenow, aria-valuemin und aria-valuemax, die den aktuellen Breitenwert sowie die erlaubten Grenzen für Screenreader zugänglich machen, ähnlich wie bei einem Slider-Element. Der Fokus-Zustand des Handles sollte visuell mindestens so deutlich sichtbar sein wie der Hover-Zustand, meist über einen zusätzlichen Tailwind-Fokus-Ring, da Tastaturnutzer sonst nicht erkennen können, ob der Handle überhaupt fokussiert und damit für die Pfeiltasten-Steuerung bereit ist.
| Eigenschaft | Typischer Wert | Zweck | Wo gesetzt |
|---|---|---|---|
| Mindestbreite links | 180px | Verhindert unbrauchbar schmalen Datei-Baum | JavaScript-Clamp beim Draggen |
| Maximalbreite links | 480px | Verhindert Editor-Bereich, der zu schmal wird | JavaScript-Clamp beim Draggen |
| Handle-Breite sichtbar | 4px | Dezente Trennlinie im Ruhezustand | Tailwind-Utility-Klasse |
| Handle-Trefferfläche | 8-12px | Großzügigere Klickfläche als sichtbare Breite | Padding oder Pseudo-Element |
| Tastatur-Schrittweite | 20-40px | Pfeiltasten-Steuerung ohne Maus | Keydown-Handler auf dem Separator |
Mironsoft
Tailwind-CSS-Architektur, Design-Systeme und Performance
Tailwind-Frontends, die trotz tausender Utility-Klassen wartbar bleiben?
Wir prüfen bestehende Tailwind-Projekte auf aufgeblähte Klassenlisten, inkonsistente Design-Tokens und ungenutzte CSS-Reste und bauen daraus ein Design-System, das sich sauber skaliert statt mit jeder Komponente unübersichtlicher zu werden.
Design-System-Review
Tokens, Spacing-Skala und Komponentenkonsistenz auf Wartbarkeit prüfen.
Performance-Optimierung
CSS-Bundle-Größe, Purge-Konfiguration und Ladezeiten systematisch reduzieren.
Component-Architektur
Wiederverwendbare, gut strukturierte Komponenten statt Klassenlisten-Wildwuchs aufbauen.
10. Zusammenfassung
Split-Pane und Resizable Panels mit Tailwind: Das Wichtigste auf einen Blick
Grid-Basis
Statisches Tailwind-Grid mit einer per CSS-Custom-Property dynamisch gesetzten Spaltenbreite.
Constraints
Mindest- und Maximalbreite direkt in der JavaScript-Berechnung begrenzen, nicht nur nachträglich per CSS.
Cursor & Auswahl
cursor-col-resize und select-none global auf dem body während der aktiven Drag-Interaktion setzen.
Barrierefreiheit
role=separator mit Pfeiltasten-Steuerung und aria-valuenow als vollwertige Alternative zur Maus.