Sortierbare Tabellenspalten mit Alpine.js ohne Bibliothek
AI generated
x-data
Alpine
Alpine.js · Tabellen · Case Study
Sortierbare Tabellenspalten mit Alpine.js
Klick auf den Spaltenkopf, ganz ohne Bibliothek

Sortierbare Tabellenspalten gehören zu den am häufigsten gebrauchten Datenlisten Funktionen, landen aber oft über eine komplette DataTables Bibliothek im Projekt. Mit einer einzigen Alpine.js x-data Komponente, einer generischen Vergleichsfunktion und ein paar Zeilen für die Sortierrichtung entsteht dieselbe Funktionalität ganz ohne zusätzliche Abhängigkeit.

20 Min. Lesezeit x-data · Array.prototype.sort · computed Property Alpine.js 3.x

1. Warum sortierbare Tabellenspalten oft überdimensioniert gelöst werden

Sobald eine Tabelle mit mehreren Spalten und mehr als einer Handvoll Zeilen im Projekt auftaucht, greifen viele Teams reflexartig zu DataTables, ag Grid oder einer ähnlichen umfangreichen Tabellen Bibliothek. Diese Bibliotheken bringen Pagination, Filterung, Export und viele weitere Funktionen mit, oft aber auch fünfzig bis hundert Kilobyte zusätzliches JavaScript, nur um sortierbare Tabellenspalten auf einen Klick zu realisieren.

Die eigentliche Logik hinter sortierbaren Tabellenspalten ist überschaubar: eine Referenz auf die aktuell sortierte Spalte, ein Flag für die Sortierrichtung, und eine generische Vergleichsfunktion, die je nach Datentyp der Spalte korrekt sortiert. Mit Alpine.js lässt sich diese Funktionalität in einer einzigen x-data Komponente umsetzen, die exakt zur bestehenden Tabellenstruktur passt, ohne ein komplettes Grid System mit eigenem CSS und eigenen Konventionen einzuführen.

Dieser Artikel baut vollständig sortierbare Tabellenspalten auf: von der Grundstruktur über typsichere Vergleiche für Zahlen, Daten und Text, visuelle Sortierindikatoren, mehrstufige Sortierung mit Tiebreaker Spalte bis hin zu vollständiger Tastatur und Screenreader Unterstützung.

2. Grundstruktur: Spaltenkopf, Klick Handler und State

Die Grundstruktur für sortierbare Tabellenspalten braucht zwei State Variablen: sortColumn speichert den Feldnamen der aktuell sortierten Spalte, sortDirection speichert entweder asc oder desc. Jeder Spaltenkopf bekommt einen @click Handler, der beim Klick prüft, ob dieselbe Spalte bereits aktiv ist. Ist sie es, wird nur die Richtung umgekehrt, ansonsten wird die neue Spalte mit der aufsteigenden Richtung als Standard gesetzt.

Diese Struktur für sortierbare Tabellenspalten hält den Zustand minimal: nur zwei Variablen steuern die komplette Sortierung der Tabelle, unabhängig davon, wie viele Spalten tatsächlich existieren. Die eigentlichen Zeilen werden nie mutiert, sondern über eine computed Property als neue, sortierte Kopie des Original Arrays bereitgestellt, was Seiteneffekte vermeidet und die Komponente vorhersehbar macht.


<table x-data="sortableTable()" class="w-full text-sm border-collapse">
  <thead>
    <tr>
      <th class="cursor-pointer select-none p-3" @click="toggleSort('name')">
        Name <span x-text="sortIndicator('name')"></span>
      </th>
      <th class="cursor-pointer select-none p-3" @click="toggleSort('price')">
        Preis <span x-text="sortIndicator('price')"></span>
      </th>
      <th class="cursor-pointer select-none p-3" @click="toggleSort('createdAt')">
        Erstellt <span x-text="sortIndicator('createdAt')"></span>
      </th>
    </tr>
  </thead>
  <tbody>
    <template x-for="row in sortedRows" :key="row.id">
      <tr class="border-t border-slate-100">
        <td class="p-3" x-text="row.name"></td>
        <td class="p-3" x-text="row.price"></td>
        <td class="p-3" x-text="row.createdAt"></td>
      </tr>
    </template>
  </tbody>
</table>

3. Die Sortierlogik: computed Property mit Array.prototype.sort

Der Kern sortierbarer Tabellenspalten ist eine computed Property sortedRows, die bei jedem Zugriff eine neue, sortierte Kopie der Originaldaten zurückgibt. Wichtig ist, Array.prototype.sort niemals direkt auf das Original Array anzuwenden, da diese Methode das Array in place verändert. Stattdessen wird zuerst mit dem Spread Operator eine flache Kopie erstellt, die dann sortiert wird, damit die Originaldaten unangetastet bleiben.

Für sortierbare Tabellenspalten mit Alpine.js reicht eine generische Vergleichsfunktion, die den Wert des aktuell sortierten Feldes aus beiden zu vergleichenden Zeilen extrahiert und je nach Richtung das Ergebnis von localeCompare oder einer numerischen Subtraktion zurückgibt. Diese eine Funktion deckt bereits die überwiegende Mehrheit realer Tabellen Anwendungsfälle ab.


function sortableTable() {
  return {
    rows: [
      { id: 1, name: 'Widget A', price: 29.99, createdAt: '2026-01-15' },
      { id: 2, name: 'Widget B', price: 14.5, createdAt: '2026-03-02' },
      { id: 3, name: 'Widget C', price: 49.0, createdAt: '2025-11-20' },
    ],
    sortColumn: 'name',
    sortDirection: 'asc',

    // Never mutate the original array — sort() mutates in place
    get sortedRows() {
      const copy = [...this.rows];
      const direction = this.sortDirection === 'asc' ? 1 : -1;

      return copy.sort((a, b) => {
        const valueA = a[this.sortColumn];
        const valueB = b[this.sortColumn];

        if (typeof valueA === 'number' && typeof valueB === 'number') {
          return (valueA - valueB) * direction;
        }
        return String(valueA).localeCompare(String(valueB)) * direction;
      });
    },

    toggleSort(column) {
      if (this.sortColumn === column) {
        this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
      } else {
        this.sortColumn = column;
        this.sortDirection = 'asc';
      }
    },
  };
}

4. Sortierrichtung umschalten: auf- und absteigend

Nutzer erwarten von sortierbaren Tabellenspalten, dass ein erster Klick aufsteigend sortiert und ein zweiter Klick auf dieselbe Spalte die Richtung umkehrt. Die toggleSort Methode aus dem vorherigen Abschnitt implementiert genau dieses Verhalten: Ist die geklickte Spalte bereits die aktive Sortierspalte, wird lediglich sortDirection zwischen asc und desc gewechselt. Wird eine andere Spalte geklickt, startet die neue Sortierung immer aufsteigend, unabhängig von der vorherigen Richtung der alten Spalte.

Ein Detail, das bei sortierbaren Tabellenspalten oft übersehen wird: manche Anwendungsfälle profitieren von einem dritten Zustand, dem Zurücksetzen auf die ursprüngliche, unsortierte Reihenfolge nach zwei Klicks. Dafür wird zusätzlich zu asc und desc ein dritter Wert null eingeführt, bei dem sortedRows die Originaldaten unverändert zurückgibt, ohne sort() überhaupt aufzurufen.


function sortableTable() {
  return {
    rows: [ /* ... */ ],
    sortColumn: null,
    sortDirection: null, // 'asc' | 'desc' | null

    // Three-state cycle: asc -> desc -> null (original order) -> asc ...
    toggleSort(column) {
      if (this.sortColumn !== column) {
        this.sortColumn = column;
        this.sortDirection = 'asc';
        return;
      }
      if (this.sortDirection === 'asc') {
        this.sortDirection = 'desc';
      } else if (this.sortDirection === 'desc') {
        this.sortColumn = null;
        this.sortDirection = null;
      } else {
        this.sortDirection = 'asc';
      }
    },

    get sortedRows() {
      if (!this.sortColumn) return this.rows;
      const copy = [...this.rows];
      const direction = this.sortDirection === 'asc' ? 1 : -1;
      return copy.sort((a, b) => String(a[this.sortColumn]).localeCompare(String(b[this.sortColumn])) * direction);
    },
  };
}

5. Typ sichere Vergleiche: Zahlen, Daten und Text

Ein häufiger Fehler bei sortierbaren Tabellenspalten ist, alle Werte pauschal über localeCompare als Text zu vergleichen. Das führt bei numerischen Spalten zu falschen Ergebnissen, weil String Vergleiche lexikografisch statt numerisch arbeiten: "10" würde vor "9" einsortiert, weil das Zeichen "1" im Alphabet vor "9" steht. Für sortierbare Tabellenspalten mit gemischten Datentypen ist deshalb eine typspezifische Vergleichsfunktion Pflicht.

Datumswerte bringen eine zusätzliche Herausforderung mit: als String im Format 2026-03-02 funktioniert ein lexikografischer Vergleich zwar korrekt, weil das ISO Format sich in dieser Hinsicht wie eine Zahl verhält, bei anderen Datumsformaten wie 02.03.2026 jedoch nicht. Für sortierbare Tabellenspalten mit Datumsangaben empfiehlt sich deshalb, Werte vor dem Vergleich explizit über new Date() in Zeitstempel umzuwandeln, unabhängig vom Anzeigeformat.


// Column type registry drives the correct comparison strategy per field
const columnTypes = {
  name: 'string',
  price: 'number',
  createdAt: 'date',
};

function compareValues(valueA, valueB, type) {
  switch (type) {
    case 'number':
      return valueA - valueB;
    case 'date':
      return new Date(valueA).getTime() - new Date(valueB).getTime();
    case 'string':
    default:
      return String(valueA).localeCompare(String(valueB), 'de', { sensitivity: 'base' });
  }
}

function sortableTable() {
  return {
    rows: [ /* ... */ ],
    sortColumn: 'name',
    sortDirection: 'asc',

    get sortedRows() {
      const copy = [...this.rows];
      const direction = this.sortDirection === 'asc' ? 1 : -1;
      const type = columnTypes[this.sortColumn] || 'string';
      return copy.sort((a, b) => compareValues(a[this.sortColumn], b[this.sortColumn], type) * direction);
    },
  };
}

6. Visuelle Sortierindikatoren im Spaltenkopf

Sortierbare Tabellenspalten ohne visuelles Feedback verwirren Nutzer, weil unklar bleibt, welche Spalte gerade die aktive Sortierung bestimmt und in welche Richtung sortiert wird. Ein kleiner Pfeil neben dem Spaltentitel, der zwischen nach oben und nach unten wechselt, löst dieses Problem mit minimalem Aufwand. Die Methode sortIndicator gibt je nach Zustand der Spalte ein passendes Unicode Zeichen oder eine leere Zeichenkette zurück.

Für sortierbare Tabellenspalten mit Tailwind CSS lässt sich derselbe Effekt auch über bedingte Klassen auf einem SVG Pfeil Icon erreichen, das über :class zwischen einer Rotation von null und 180 Grad wechselt. Diese Variante wirkt in modernen Interfaces oft etwas eleganter als reine Unicode Pfeile, weil sich Größe und Farbe konsistent mit dem restlichen Design System steuern lassen.


function sortableTable() {
  return {
    rows: [ /* ... */ ],
    sortColumn: 'name',
    sortDirection: 'asc',

    sortIndicator(column) {
      if (this.sortColumn !== column) return '';
      return this.sortDirection === 'asc' ? '▲' : '▼';
    },

    isActiveSortColumn(column) {
      return this.sortColumn === column;
    },
  };
}

7. Mehrstufige Sortierung: Sekundärspalte als Tiebreaker

Bei sortierbaren Tabellenspalten mit vielen identischen Werten in der primären Sortierspalte wirkt die Reihenfolge innerhalb gleicher Gruppen oft willkürlich, weil Array.prototype.sort zwar stabil ist, die ursprüngliche Einfügereihenfolge aber selten die gewünschte sekundäre Ordnung darstellt. Eine mehrstufige Sortierung löst dieses Problem, indem bei Gleichheit der Primärspalte eine zweite, feste Vergleichsspalte als Tiebreaker herangezogen wird.

Technisch verkettet man dafür zwei Vergleichsfunktionen: Liefert der erste Vergleich 0 zurück, entscheidet der zweite Vergleich über die Reihenfolge. Für sortierbare Tabellenspalten mit Datensätzen, bei denen etwa mehrere Produkte denselben Namen, aber unterschiedliche Erstelldaten haben, sorgt dieser Ansatz für eine konsistente, nachvollziehbare Sortierung statt einer scheinbar zufälligen Reihenfolge innerhalb gleicher Gruppen.


function sortableTable() {
  return {
    rows: [ /* ... */ ],
    sortColumn: 'name',
    sortDirection: 'asc',
    tiebreakerColumn: 'createdAt', // secondary sort key when primary values are equal

    get sortedRows() {
      const copy = [...this.rows];
      const direction = this.sortDirection === 'asc' ? 1 : -1;

      return copy.sort((a, b) => {
        const primary = String(a[this.sortColumn]).localeCompare(String(b[this.sortColumn]));
        if (primary !== 0) return primary * direction;

        // Tiebreaker: always ascending, regardless of the primary direction
        return String(a[this.tiebreakerColumn]).localeCompare(String(b[this.tiebreakerColumn]));
      });
    },
  };
}

8. Barrierefreiheit: ARIA Sort Attribut und Tastatur

Für barrierefreie sortierbare Tabellenspalten braucht jeder klickbare Spaltenkopf das ARIA Attribut aria-sort mit einem der Werte ascending, descending oder none, damit Screenreader Nutzer den aktuellen Sortierzustand einer Spalte korrekt vorgelesen bekommen. Zusätzlich sollte der Spaltenkopf über tabindex="0" und einen @keydown.enter Handler auch ohne Maus bedienbar sein, da ein reiner @click Handler auf einem th Element standardmäßig nicht per Tastatur erreichbar ist.

Zusätzlich profitieren sortierbare Tabellenspalten von der Rolle columnheader und einem beschreibenden aria-label, der den Spaltennamen und die aktuelle Sortierrichtung in einem Satz zusammenfasst, etwa "Nach Preis sortiert, aufsteigend". Diese kleinen Ergänzungen sind der Unterschied zwischen einer rein visuell funktionierenden Tabelle und einer, die tatsächlich von allen Nutzergruppen bedient werden kann.


<th
  scope="col"
  tabindex="0"
  role="columnheader"
  class="cursor-pointer select-none p-3"
  @click="toggleSort('price')"
  @keydown.enter="toggleSort('price')"
  :aria-sort="sortColumn === 'price' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'"
>
  Preis <span x-text="sortIndicator('price')" aria-hidden="true"></span>
</th>

9. Sortier Ansätze im Vergleich

Für sortierbare Tabellenspalten existieren mehrere technische Umsetzungen mit unterschiedlichen Kompromissen.

Ansatz Bundle Größe Anpassbarkeit Eignung
Alpine.js computed Property 0 KB zusätzlich Vollständig Standardfall, alle Projektgrößen
DataTables circa 90 KB gzip mit jQuery Über Plugin Konfiguration Legacy Projekte mit vielen Zusatzfunktionen
ag Grid Community circa 250 KB gzip Sehr umfangreich Komplexe Enterprise Datengrids
Server seitige Sortierung 0 KB Frontend Abhängig vom Backend Sehr große Datenmengen, Paginierung nötig

Für die überwiegende Mehrheit realer Anwendungsfälle mit clientseitig bereits geladenen Daten überzeugt die Alpine.js Lösung für sortierbare Tabellenspalten durch minimale Bundle Größe und vollständige Kontrolle über Verhalten und Darstellung. Erst bei sehr großen Datenmengen, die eine serverseitige Paginierung erfordern, verschiebt sich die Sortierlogik sinnvollerweise in die Datenbank Abfrage selbst.

Mironsoft

Alpine.js Komponenten für Hyvä, Magento und individuelle Frontends

Eine eigene sortierbare Tabelle oder eine andere Alpine.js Komponente nötig?

Wir entwickeln maßgeschneiderte Alpine.js Komponenten, von sortierbaren Datentabellen über Filterlisten bis zu komplexen Formularen, sauber integriert in euer bestehendes Hyvä oder Magento Frontend.

Konzeption

Datenmodell und Sortierlogik für eure Tabelle klären

Umsetzung

Typ sichere Vergleiche, mehrstufige Sortierung und Barrierefreiheit

Integration

Saubere Einbindung in bestehende Hyvä und Magento Frontends

10. Zusammenfassung

Sortierbare Tabellenspalten lassen sich vollständig mit Alpine.js umsetzen, ohne eine schwergewichtige Tabellen Bibliothek zu laden. Zwei State Variablen für Spalte und Richtung, eine computed Property, die eine sortierte Kopie der Daten liefert, und eine typspezifische Vergleichsfunktion für Zahlen, Daten und Text bilden das komplette Fundament.

Für den produktiven Einsatz kommen visuelle Sortierindikatoren, mehrstufige Sortierung mit Tiebreaker Spalte und vollständige Barrierefreiheit über aria-sort und Tastaturunterstützung dazu. Zusammen entsteht eine Lösung für sortierbare Tabellenspalten, die in Bundle Größe und Performance jeder fertigen DataTables Bibliothek überlegen ist, solange die Daten bereits clientseitig vorliegen.

Sortierbare Tabellenspalten mit Alpine.js — Das Wichtigste auf einen Blick

State

Zwei Variablen: sortColumn und sortDirection steuern die gesamte Sortierung.

Sortierlogik

Computed Property mit Kopie des Original Arrays, niemals direkte Mutation über sort().

Typsicherheit

Spaltentyp Registry entscheidet zwischen numerischem, Datums und String Vergleich.

Barrierefreiheit

aria-sort, tabindex und @keydown.enter für vollständige Tastaturbedienung.

11. FAQ: Sortierbare Tabellenspalten mit Alpine.js

1Warum nicht sort() direkt auf Originaldaten?
sort() mutiert in place, immer zuerst eine Kopie mit dem Spread Operator erstellen.
2Wie sortiere ich gemischte Datentypen?
Über eine Spaltentyp Registry mit numerischem, Datums oder String Vergleich.
3Wie funktioniert das Umschalten der Richtung?
Klick auf aktive Spalte kehrt Richtung um, Klick auf neue Spalte startet aufsteigend.
4Wie zeige ich die aktive Spalte an?
Mit sortIndicator Methode und Pfeil Symbol im Spaltenkopf.
5Was ist mehrstufige Sortierung?
Eine Tiebreaker Spalte entscheidet bei Gleichheit der Primärspalte.
6Wie mache ich es barrierefrei?
Mit aria-sort, tabindex und keydown.enter Handler.
7Wann serverseitige Sortierung?
Sobald Paginierung nötig ist und Daten nicht vollständig vorliegen.
8Wie sortiere ich Datumswerte korrekt?
Mit new Date() in Zeitstempel umwandeln vor dem Vergleich.
9Dritter Zustand für Originalreihenfolge?
Ja, sortDirection kann zusätzlich null annehmen.
10Eigenbau oder DataTables?
Für reine Sortierung fast immer Eigenbau, wegen Bundle Größe.