Wie das Festhalten des Tabellenkopfs genau geht, warum der Tabellenkopf auf kleineren Bildschirmen oft an einer anderen Stelle andocken muss und was sich mit ein wenig JavaScript noch anstellen lässt, liest du in diesem Beitrag.

Wenn die Tabelle länger ist als der Bildschirm

In einem Kundenprojekt hatte ich es mit Tabellen zu tun, die weit über den sichtbaren Bereich (Viewport) hinausgingen. Nach ein paar Zeilen Scrollen ist der Tabellenkopf aus dem Viewport verschwunden – und damit die Information, welche Spalte eigentlich was zeigt. Man scrollt zurück nach oben, schaut nach und scrollt wieder hinunter. Bei jeder Tabelle, immer wieder.

Abhilfe schafft ein Tabellenkopf, der stehen bleibt, während die Zeilen darunter weiter durchlaufen – ein Sticky Header, also ein «klebender» Tabellenkopf. Der Sticky Header hat sich schon in früheren Kundenprojekten bewährt und wurde gut angenommen.

Die eigentliche Arbeit macht dabei eine einzige CSS-Eigenschaft. Der Rest sind zwei Details: wo der Tabellenkopf andockt und was passieren soll, sobald er angedockt ist.

Hinweis: Das Kundenprojekt war mit Bootstrap umgesetzt. Die Code-Beispiele in diesem Beitrag kommen bewusst ohne Framework aus, weil es um das Prinzip geht.

Wie bleibt der Tabellenkopf stehen?

Mit position: sticky auf dem thead und einem top-Wert, der festlegt, wo der Tabellenkopf andocken soll.

.sticky-head thead {
    position: sticky;
    top: 0;
    z-index: 2;
}

.sticky-head th {
    background-color: #fff;
}

Ein Element mit position: sticky verhält sich zunächst ganz normal und scrollt mit der Seite mit. Erreicht das Element beim Scrollen den top-Wert, bleibt es dort stehen – und zwar so lange, wie sein Elternelement noch zu sehen ist. In diesem Fall ist das die Tabelle. Ist das Ende der Tabelle erreicht, scrollt der Tabellenkopf mit ihr weg. Genau so soll es sein: Der Tabellenkopf gehört zur Tabelle und nicht an den Bildschirmrand. Damit können auch mehrere lange Tabellen auf einer Seite einen Sticky Header haben und bei keiner verliert man die Übersicht.

Die Voraussetzung in der Tabelle ist, dass die Kopfzeile im HTML auch als thead ausgezeichnet ist und nicht zum tbody gehört. Der z-index sorgt dafür, dass der Tabellenkopf über den Zeilen liegt, die unter ihm durchlaufen.

Die Hintergrundfarbe der Kopfzellen ist nicht optional: Ohne sie scheinen die Zeilen beim Scrollen durch den Tabellenkopf hindurch. Im Kundenprojekt hat Bootstrap das über die Klasse .table bereits mitgebracht, ohne Framework muss man die Hintergrundfarbe selbst setzen.

Merke: Hat ein Elternelement der Tabelle overflow: hidden oder overflow: auto, bezieht sich position: sticky auf dieses Element statt auf den Viewport. Der Tabellenkopf bleibt dann scheinbar nicht mehr stehen – ohne Fehlermeldung. Das betrifft zum Beispiel ein umschliessendes Element (Wrapper), das breite Tabellen horizontal scrollbar macht.

Warum dockt der Tabellenkopf auf kleineren Bildschirmen woanders an?

Weil dort oft eine Sticky Navbar, eine oben am Viewport fixierte Navigationsleiste, den Platz ganz oben belegt. Mit top: 0 würde der Tabellenkopf hinter ihr verschwinden – er wäre zwar angedockt, aber nicht sichtbar.

Im Kundenprojekt war das die Navbar von Bootstrap: Auf kleinen Bildschirmen klebt sie oben am Viewport, der Tabellenkopf muss also direkt darunter andocken. top ist dort nicht 0, sondern die Höhe der Navbar. Auf grösseren Bildschirmen braucht es diesen Abstand im ursprünglichen Fall nicht, dort dockt der Tabellenkopf wieder bei 0 an.

.sticky-head thead {
    position: sticky;
    top: 4rem; /* Höhe der Sticky Navbar */
    z-index: 2;
}

@media (min-width: 992px) {
    .sticky-head thead {
        top: 0;
    }
}

Die Navbar ist dabei nur ein Beispiel. top ist überall dort nicht 0, wo schon etwas oben am Viewport klebt: eine eigene Sticky-Navigation innerhalb der Seite, eine Filterleiste über der Tabelle oder die Adminleiste von WordPress, wenn man angemeldet ist. Das Prinzip bleibt gleich – der Tabellenkopf dockt unter dem an, was schon da ist.

Wozu dann noch JavaScript?

JavaScript braucht es für alles, was sich ändern soll, sobald der Tabellenkopf angedockt ist. Ob er gerade angedockt ist, verrät position: sticky von sich aus nicht.

Dafür kann ein kleines JavaScript die Klasse is-sticking auf die Tabelle setzen, sobald ihre Oberkante den Punkt erreicht, an dem der Tabellenkopf andockt. Auf kleinen Bildschirmen ist das die gemessene Höhe der Navbar, auf grösseren 0 – der Wert 992 entspricht in diesem Beispiel dem Breakpoint aus dem CSS.

function updateStickyState( tables ) {
    const navbar = document.querySelector( '.navbar' );
    let stickyOffset = 0;

    if ( window.innerWidth < 992 && navbar ) {
        stickyOffset = navbar.getBoundingClientRect().height;
    }

    tables.forEach( ( table ) => {
        table.classList.toggle(
            'is-sticking',
            table.getBoundingClientRect().top <= stickyOffset
        );
    } );
}

function initStickyHead() {
    const tables = document.querySelectorAll( 'table.sticky-head' );

    if ( ! tables.length ) {
        return;
    }

    window.addEventListener( 'scroll', () => {
        updateStickyState( tables );
    }, {
        passive: true
    } );

    window.addEventListener( 'resize', () => {
        updateStickyState( tables );
    }, {
        passive: true
    } );

    updateStickyState( tables );
}

if ( document.readyState !== 'loading' ) {
    initStickyHead();
} else {
    window.addEventListener( 'DOMContentLoaded', initStickyHead );
}

Was man mit der CSS-Klasse macht, ist offen. Im Kundenprojekt hatten die Tabellen abgerundete Ecken. Angedockt wirken die oberen Rundungen des Tabellenkopfs fehl am Platz, also wurden sie im Zustand is-sticking zurückgesetzt:

.sticky-head.is-sticking th:first-child {
    border-top-left-radius: 0;
}

.sticky-head.is-sticking th:last-child {
    border-top-right-radius: 0;
}

Genauso gut lässt sich der Tabellenkopf im angedockten Zustand einfärben oder mit einem Schatten von den Zeilen abheben. Da kann man kreativ sein.

Kurz erklärt: Beim Scrollen und bei Grössenänderungen des Fensters prüft das Skript nur die Tabellen selbst, nicht jede einzelne Zeile. Bei weniger als zehn Tabellen mit je hundert Zeilen ist damit keine spürbare Auswirkung auf die Performance zu erwarten.

Lässt sich der Tabellenkopf schon vor der letzten Zeile aus dem Viewport animieren?

Nein, nicht ohne Ruckeln – und nicht mit vertretbarem Aufwand. Mit position: sticky bleibt der Tabellenkopf stehen, bis die Tabelle zu Ende ist. Die letzte Zeile läuft also noch unter ihm durch, bevor er mit der Tabelle wegscrollt.

Wer jetzt denkt, es wäre aber doch schön, wenn der Tabellenkopf schon vor der letzten Zeile nach oben wegscrollt: Ich habe Versuche dazu angestellt und ohne Ruckeln war diese Animation in verschiedenen Varianten nicht möglich. Zudem ist es ein extremer Aufwand – Höhen müssen berechnet und festgehalten, Scroll-Trigger definiert werden und vieles mehr – für eine Funktion, die sonst kaum Leistung im Browser braucht, weil CSS allein die Arbeit macht. Ähnlich war es schon bei meiner klickbaren Beitragsvorschau, wo am Ende ein CSS-Pseudo-Element ein ganzes Skript ersetzt hat. Dieses eine kleine visuelle Defizit bleibt also.

Fazit

Ein Tabellenkopf, der stehen bleibt, ist kein grosses Feature. Aber wenn man durch eine lange Tabelle scrollt, merkt man sofort, ob der Tabellenkopf mitkommt oder nicht. Der Aufwand, den Tabellenkopf als Sticky Header umzusetzen, ist gering, der Nutzen allerdings riesig. Man muss ihm nur sagen, wo er stehen bleiben soll. Bitte.