Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting

Schwierigkeit: 🟡 Mittel
Lesezeit: 25 Minuten
Voraussetzungen: Spring Boot Basics, HTML/CSS Grundlagen


⚡ Das Wichtigste in 30 Sekunden

Dein Problem: Du brauchst eine Sternchenbewertung für dein Projekt – aber du willst keine externe Library einbinden, die du nicht verstehst.

Die Lösung: Wir bauen alles selbst – mit purem CSS, Thymeleaf-Fragments und einem sauberen Controller.

Heute lernst du:

  • ✅ Sterne nur mit CSS zeichnen (keine Icons, keine Fonts)
  • ✅ Interaktive Hover-Effekte ohne JavaScript
  • ✅ Ein wiederverwendbares Thymeleaf-Fragment
  • ✅ Controller-Anbindung für Form-Submits

Für wen ist dieser Artikel?

  • 🌱 Anfänger: Du lernst CSS-Tricks und Thymeleaf-Grundlagen
  • 🌿 Erfahrene: Du bekommst ein production-ready Fragment
  • 🌳 Profis: Im Bonus: Accessibility und Half-Stars

Zeit-Investment: 25 Minuten lesen, 30 Minuten nachbauen


👋 Elyndra: „Manchmal ist selbst bauen der beste Weg“

Hi! 👋

Elyndra hier. Letzte Woche kam Nova zu mir: „Ich brauche eine Sternchenbewertung. Welche Library nehm ich?“

Meine Antwort? „Keine.“

Nicht, weil Libraries schlecht sind. Sondern weil eine Sternchenbewertung so simpel ist, dass du dabei mehr lernst, wenn du sie selbst baust. Es ist wie beim Fachwerkhaus – bevor du moderne Werkzeuge nutzt, solltest du verstehen, wie die Handwerker früher ohne sie ausgekommen sind.

Das Schöne daran: Am Ende hast du Code, den du zu 100% verstehst. Keine Black Box. Keine Dependency, die in zwei Jahren deprecated ist.

Lass uns das gemeinsam bauen! 🚀


🟢 GRUNDLAGEN

Was wollen wir eigentlich bauen?

Bevor wir Code schreiben, lass uns klar definieren, was wir brauchen:

Die Anforderungen:

  1. 5 Sterne – klassisch, jeder kennt es
  2. Klickbar – der User kann eine Bewertung abgeben
  3. Hover-Effekt – visuelle Rückmeldung beim Überfahren
  4. Kein JavaScript – nur CSS und HTML (ja, das geht!)
  5. Wiederverwendbar – als Thymeleaf-Fragment für mehrere Seiten

Was wir NICHT bauen (heute):

  • Persistenz in der Datenbank (wäre Teil 2)
  • AJAX-Updates ohne Page-Reload
  • Half-Stars (aber im Bonus zeige ich den Ansatz)

Die Architektur im Überblick

Sternchenbewertung

Was macht dieser Überblick?

Er zeigt dir den Datenfluss: Der User klickt einen Stern → ein HTML-Formular wird abgeschickt → der Controller empfängt die Bewertung. Keine Magie, keine versteckten Komponenten.

Der CSS-Trick: Sterne ohne Icons

Hier kommt der erste Aha-Moment. Wir brauchen keine Icon-Library wie FontAwesome. CSS kann Sterne zeichnen – mit einem simplen Unicode-Zeichen.

.star::before {
    content: "★";  /* Unicode: Gefüllter Stern */
    font-size: 2rem;
    color: #ddd;   /* Grau = nicht ausgewählt */
}

.star.active::before {
    color: #ffc107; /* Gold = ausgewählt */
}

Was macht dieser Code?

Das ::before Pseudo-Element fügt vor jedem Element mit der Klasse .star einen Stern ein. Der Stern selbst ist das Unicode-Zeichen (U+2605).

Wie funktioniert das im Detail?

Das content-Property ist der Schlüssel. Es erlaubt dir, Inhalte rein über CSS einzufügen – ohne das HTML zu ändern. Das bedeutet: Dein HTML bleibt semantisch sauber, während CSS sich um die Darstellung kümmert.

Die Farbsteuerung passiert über die Klasse active. Ein Stern ohne diese Klasse ist grau (#ddd). Mit der Klasse wird er gold (#ffc107). Thymeleaf entscheidet später, welche Sterne active bekommen.

Wichtig zu verstehen: Wir könnten auch ein Bild oder einen Icon-Font nehmen. Aber Unicode-Zeichen haben Vorteile:

  • Null zusätzliche HTTP-Requests
  • Skalieren perfekt (es ist Text!)
  • Funktionieren in jedem Browser seit IE6

🟡 PROFESSIONALS

Das komplette CSS

Jetzt bauen wir das vollständige Stylesheet. Hier kommt der Trick für den Hover-Effekt ohne JavaScript:

/* ============================================
   STAR RATING - Pure CSS Implementation
   ============================================ */

.star-rating {
    display: inline-flex;
    flex-direction: row-reverse;  /* Der Trick! */
    gap: 0.25rem;
}

.star-rating input[type="radio"] {
    display: none;  /* Radio-Buttons verstecken */
}

.star-rating label {
    cursor: pointer;
    font-size: 2rem;
    color: #ddd;
    transition: color 0.2s ease;
}

.star-rating label::before {
    content: "★";
}

/* Ausgewählter Stern und alle davor */
.star-rating input:checked ~ label {
    color: #ffc107;
}

/* Hover: Aktueller Stern und alle davor */
.star-rating label:hover,
.star-rating label:hover ~ label {
    color: #ffdb4d;
}

/* Nach Hover: Bereits ausgewählte behalten Farbe */
.star-rating input:checked ~ label:hover ~ label {
    color: #ffc107;
}

Was macht dieser Code?

Er erzeugt eine vollständig interaktive Sternchenbewertung – nur mit CSS. Kein JavaScript nötig.

Wie funktioniert das im Detail?

Der flex-direction: row-reverse Trick

Das ist das Herzstück. Warum reverse? Weil CSS nur nachfolgende Geschwister-Elemente selektieren kann (mit ~), nicht vorherige. Indem wir die Reihenfolge umkehren, können wir sagen: „Wenn Stern 3 gehovert wird, färbe auch Stern 4 und 5“ – was visuell wie „Stern 1, 2 und 3“ aussieht.

HTML-Reihenfolge:    5 ← 4 ← 3 ← 2 ← 1
Visuelle Anzeige:    1 → 2 → 3 → 4 → 5

In der Praxis bedeutet das: Du schreibst die Radio-Buttons von 5 nach 1 im HTML. CSS dreht sie um. Hover auf „Stern 3“ selektiert die nachfolgenden Labels (4 und 5 im HTML = 2 und 1 visuell).

Der input:checked ~ label Selektor

Das Tilde-Zeichen (~) ist der „General Sibling Combinator“. Er selektiert alle Geschwister-Elemente, die nach dem angegebenen Element kommen.

input:checked ~ label bedeutet: „Alle Labels, die nach einem ausgewählten Radio-Button kommen.“

Die versteckten Radio-Buttons

Wir nutzen <input type="radio"> für die Logik (nur einer kann ausgewählt sein), verstecken sie aber visuell. Die Labels sind klickbar und mit for="star-X" verknüpft.

Das Thymeleaf-Fragment

Jetzt machen wir das Ganze wiederverwendbar. Ein Fragment ist wie eine Komponente – du definierst es einmal und nutzt es überall.

<!-- fragments/rating.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>

<!-- 
    Star Rating Fragment
    Parameter:
    - name: Name des Form-Feldes (z.B. "rating")
    - value: Aktueller Wert (0-5)
    - readonly: true/false - nur Anzeige oder klickbar?
-->
<div th:fragment="stars(name, value, readonly)" 
     class="star-rating"
     th:classappend="${readonly} ? 'readonly' : ''">
    
    <!-- Stern 5 (höchste Bewertung) -->
    <input type="radio" 
           th:id="${name + '-5'}" 
           th:name="${name}" 
           value="5"
           th:checked="${value == 5}"
           th:disabled="${readonly}">
    <label th:for="${name + '-5'}" title="5 Sterne"></label>
    
    <!-- Stern 4 -->
    <input type="radio" 
           th:id="${name + '-4'}" 
           th:name="${name}" 
           value="4"
           th:checked="${value == 4}"
           th:disabled="${readonly}">
    <label th:for="${name + '-4'}" title="4 Sterne"></label>
    
    <!-- Stern 3 -->
    <input type="radio" 
           th:id="${name + '-3'}" 
           th:name="${name}" 
           value="3"
           th:checked="${value == 3}"
           th:disabled="${readonly}">
    <label th:for="${name + '-3'}" title="3 Sterne"></label>
    
    <!-- Stern 2 -->
    <input type="radio" 
           th:id="${name + '-2'}" 
           th:name="${name}" 
           value="2"
           th:checked="${value == 2}"
           th:disabled="${readonly}">
    <label th:for="${name + '-2'}" title="2 Sterne"></label>
    
    <!-- Stern 1 (niedrigste Bewertung) -->
    <input type="radio" 
           th:id="${name + '-1'}" 
           th:name="${name}" 
           value="1"
           th:checked="${value == 1}"
           th:disabled="${readonly}">
    <label th:for="${name + '-1'}" title="1 Stern"></label>
    
</div>

</body>
</html>

Was macht dieses Fragment?

Es kapselt die gesamte Sternchenbewertung in eine wiederverwendbare Komponente mit drei Parametern.

Wie funktioniert das im Detail?

Die Parameter

ParameterTypBeschreibung
nameStringDer Name für das Formularfeld. Wichtig für den Controller.
valueIntegerDer aktuelle Wert (0-5). Bei 0 ist nichts ausgewählt.
readonlyBooleanWenn true, kann der User nichts anklicken. Nur Anzeige.

Die Reihenfolge (5 → 1)

Erinnerst du dich an den row-reverse Trick? Deshalb müssen wir im HTML mit 5 anfangen und bei 1 enden. CSS dreht es dann visuell um.

Die th:checked Bedingung

th:checked="${value == 5}" prüft: Ist der übergebene Wert gleich 5? Wenn ja, ist dieser Radio-Button vorausgewählt.

Die th:disabled Bedingung

Für reine Anzeige (z.B. „Durchschnittsbewertung“) setzt du readonly=true. Dann werden die Inputs deaktiviert und der User kann nichts ändern.

Der Controller

Jetzt verbinden wir Frontend und Backend:

package de.javafleet.rating.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class RatingController {

    // Simulierter aktueller Wert (in echt: aus der Datenbank)
    private int currentRating = 0;

    /**
     * Zeigt die Seite mit der Sternchenbewertung
     */
    @GetMapping("/")
    public String showRatingPage(Model model) {
        model.addAttribute("rating", currentRating);
        model.addAttribute("productName", "Java Fleet Kaffeetasse");
        return "rating";
    }

    /**
     * Verarbeitet die abgegebene Bewertung
     */
    @PostMapping("/rate")
    public String submitRating(
            @RequestParam("rating") int rating,
            RedirectAttributes redirectAttributes) {
        
        // Validierung: Nur Werte von 1-5 akzeptieren
        if (rating < 1 || rating > 5) {
            redirectAttributes.addFlashAttribute("error", 
                "Ungültige Bewertung. Bitte wähle 1-5 Sterne.");
            return "redirect:/";
        }
        
        // Bewertung speichern (hier nur in-memory)
        this.currentRating = rating;
        
        // Erfolgsmeldung
        redirectAttributes.addFlashAttribute("success", 
            "Danke für deine Bewertung: " + rating + " Sterne!");
        
        return "redirect:/";
    }
}

Was macht dieser Controller?

Er bietet zwei Endpunkte: Einen zum Anzeigen der Seite (GET) und einen zum Verarbeiten der Bewertung (POST).

Wie funktioniert das im Detail?

Die @RequestParam Annotation

Spring mappt automatisch den Formularwert rating auf den Parameter. Wenn der User Stern 4 klickt und das Formular abschickt, kommt rating=4 im Controller an.

Das Post-Redirect-Get Pattern

Nach dem POST machen wir einen Redirect (return "redirect:/"). Warum? Wenn der User F5 drückt, würde ohne Redirect das Formular erneut abgeschickt. Mit Redirect wird nur die GET-Anfrage wiederholt – sicherer und benutzerfreundlicher.

Die Flash Attributes

redirectAttributes.addFlashAttribute() überlebt genau einen Redirect. Perfekt für Erfolgsmeldungen oder Fehler, die einmal angezeigt und dann vergessen werden sollen.

In der Praxis bedeutet das: Der Flow ist:

  1. User klickt Stern 4
  2. Browser sendet POST an /rate mit rating=4
  3. Controller validiert, speichert, setzt Flash-Attribute
  4. Redirect zu /
  5. Browser lädt Seite neu, zeigt Erfolgsmeldung

Die Hauptseite

Jetzt fügen wir alles zusammen:

<!-- templates/rating.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Produktbewertung</title>
    <link rel="stylesheet" th:href="@{/css/star-rating.css}">
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 600px;
            margin: 2rem auto;
            padding: 0 1rem;
            background: #f5f5f5;
        }
        .card {
            background: white;
            border-radius: 12px;
            padding: 2rem;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        h1 { margin-top: 0; }
        .message {
            padding: 1rem;
            border-radius: 8px;
            margin-bottom: 1rem;
        }
        .message.success {
            background: #d4edda;
            color: #155724;
        }
        .message.error {
            background: #f8d7da;
            color: #721c24;
        }
        .rating-form {
            display: flex;
            flex-direction: column;
            gap: 1rem;
        }
        button {
            background: #007bff;
            color: white;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 8px;
            font-size: 1rem;
            cursor: pointer;
            transition: background 0.2s;
        }
        button:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>

<div class="card">
    <h1 th:text="${productName}">Produktname</h1>
    
    <!-- Erfolgsmeldung -->
    <div th:if="${success}" class="message success" th:text="${success}"></div>
    
    <!-- Fehlermeldung -->
    <div th:if="${error}" class="message error" th:text="${error}"></div>
    
    <!-- Bewertungsformular -->
    <form action="/rate" method="post" class="rating-form">
        
        <p>Wie bewertest du dieses Produkt?</p>
        
        <!-- Fragment einbinden -->
        <div th:replace="~{fragments/rating :: stars('rating', ${rating}, false)}"></div>
        
        <button type="submit">Bewertung abgeben</button>
    </form>
    
    <!-- Beispiel: Readonly-Anzeige -->
    <hr style="margin: 2rem 0;">
    <p><strong>Durchschnittsbewertung:</strong></p>
    <div th:replace="~{fragments/rating :: stars('avg', 4, true)}"></div>
    <p style="color: #666; font-size: 0.9rem;">4.0 von 5 Sternen (127 Bewertungen)</p>
    
</div>

</body>
</html>

Was macht diese Seite?

Sie zeigt ein Formular mit der Sternchenbewertung und demonstriert beide Modi: editierbar (oben) und readonly (unten).

Wie funktioniert das im Detail?

Der Fragment-Aufruf

<div th:replace="~{fragments/rating :: stars('rating', ${rating}, false)}"></div>

Diese Zeile ist der Schlüssel zur Wiederverwendbarkeit:

  • ~{fragments/rating → Suche in templates/fragments/rating.html
  • :: stars(...) → Nimm das Fragment namens „stars“
  • 'rating' → Der Formularfeld-Name
  • ${rating} → Der aktuelle Wert aus dem Model
  • false → Nicht readonly, also klickbar

Der Unterschied: replace vs. insert

th:replace ersetzt das komplette <div> durch den Fragment-Inhalt. th:insert würde das Fragment in das div packen. Für unseren Zweck ist replace sauberer.


🔵 BONUS

Accessibility verbessern

Sternchenbewertungen sind oft ein Accessibility-Problem. Hier die Quick Wins:

<div class="star-rating" role="radiogroup" aria-label="Produktbewertung">
    <input type="radio" 
           id="rating-5" 
           name="rating" 
           value="5"
           aria-label="5 von 5 Sternen">
    <label for="rating-5" aria-hidden="true"></label>
    <!-- ... -->
</div>

Was bringt das?

  • role="radiogroup" sagt Screenreadern: „Das ist eine Gruppe von Radio-Buttons“
  • aria-label auf den Inputs gibt jedem Stern einen verständlichen Namen
  • aria-hidden="true" auf den Labels verhindert, dass der Stern-Charakter vorgelesen wird

Ansatz für Half-Stars

Falls du halbe Sterne brauchst (z.B. für Durchschnittswerte), hier der CSS-Ansatz:

.star-rating.half-stars label {
    position: relative;
    width: 2rem;
}

.star-rating.half-stars label::before {
    content: "☆";  /* Leerer Stern als Basis */
    position: absolute;
}

.star-rating.half-stars label::after {
    content: "★";
    position: absolute;
    width: 50%;
    overflow: hidden;  /* Zeigt nur die linke Hälfte */
    color: #ffc107;
}

Das Prinzip: Ein leerer Stern als Hintergrund, ein gefüllter Stern darüber – aber mit overflow: hidden auf 50% Breite beschnitten.

Andere Blogbeiträge über SpringBoot hier.


💬 Real Talk: Warum keine Library?

Java Fleet Küche, 13:15 Uhr. Nova scrollt durch npm, Elyndra holt sich einen Tee.


Nova: „Elyndra, ich hab jetzt drei Libraries für Star-Ratings gefunden. Welche soll ich nehmen?“

Elyndra: „Zeig mal… 47KB, 23KB, 89KB. Für fünf goldene Sterne?“

Nova: „Naja, die haben halt Features. Animations, Themes, Accessibility…“

Elyndra: „Hast du die Features gebraucht?“

Nova: scrollt „…Eigentlich nicht. Ich brauch nur klickbare Sterne.“

Elyndra: „Dann bau es selbst. 50 Zeilen CSS, 30 Zeilen HTML. Keine Dependency, keine Security-Updates, keine Breaking Changes in Version 3.0.“

Nova: „Aber was wenn ich später doch mehr brauche?“

Elyndra: „Dann erweiterst du deinen eigenen Code. Und du verstehst jede Zeile. Das ist mehr wert als jede Library.“

Nova nickt langsam.

Nova: „Legacy-Code-Archäologie im Kleinen, oder?“

Elyndra: lächelt „Genau. Verstehen vor Hinzufügen.“


❓ Häufig gestellte Fragen

Frage 1: Warum funktioniert der Hover-Effekt nicht in Safari?

Ältere Safari-Versionen haben Probleme mit dem ~ Selektor bei :hover. Fix: Füge -webkit-tap-highlight-color: transparent; zum Label hinzu und teste auf iOS.

Frage 2: Kann ich die Sternfarbe ändern?

Klar! Ändere #ffc107 (gold) zu jeder Farbe. Für verschiedene Themes kannst du CSS-Variablen nutzen:

.star-rating {
    --star-color: #ffc107;
    --star-color-hover: #ffdb4d;
}

Frage 3: Wie speichere ich die Bewertung in der Datenbank?

Im Controller, nach der Validierung:

// Statt: this.currentRating = rating;
ratingRepository.save(new Rating(productId, userId, rating));

Du brauchst eine Entity, ein Repository und eine Service-Schicht. Das wäre Stoff für einen eigenen Artikel.

Frage 4: Funktioniert das auch mit Thymeleaf 2?

Die Fragment-Syntax hat sich geändert. In Thymeleaf 2 war es th:include statt th:replace und ~{...} gab es nicht. Upgrade auf Thymeleaf 3 – es lohnt sich.

Frage 5: Warum Radio-Buttons und nicht Checkboxen?

Radio-Buttons erlauben nur eine Auswahl pro Gruppe. Bei Checkboxen könnte der User theoretisch mehrere Sterne gleichzeitig ankreuzen. Semantisch korrekt = weniger Edge Cases.

Frage 6: Kann ich das auch mit Vue.js / React nutzen?

Das CSS funktioniert überall. Das Thymeleaf-Fragment ersetzt du durch eine Vue/React-Komponente. Die Logik bleibt gleich – nur die Template-Syntax ändert sich.

Frage 7: Bernd meinte mal, CSS-only Lösungen seien „overengineered für simple Probleme“. Hat er recht?

Real talk? Lowkey ja – wenn du nur einmal Sterne brauchst. Aber sobald du das Zweite Mal kopierst, ist ein sauberes Fragment besser als Copy-Paste-Code. Bernd würde das auch so sehen – er hasst Duplikation mehr als „Overengineering“. 😏


📦 Downloads

Alle Code-Beispiele zum Herunterladen:

ProjektFür wen?Download
star-rating-demo.zip🟢 Alle Levels⬇️ Download

Quick Start:

# 1. ZIP entpacken
unzip star-rating-demo.zip
cd star-rating-demo

# 2. Mit Maven starten
./mvnw spring-boot:run

# 3. Browser öffnen
# http://localhost:8080

Probleme? Schau in die Troubleshooting-Sektion unten.


🔗 Weiterführende Links

📚 Für Einsteiger

RessourceBeschreibung
Thymeleaf Getting StartedOffizielle Einführung
CSS Selectors ExplainedMDN Doku zu Selektoren
Spring Boot TutorialOffizieller Quick Start

🛠️ Offizielle Dokumentation


🛠️ Troubleshooting

„Die Sterne werden nicht angezeigt“

Prüfe:

  1. Ist star-rating.css in /static/css/?
  2. Stimmt der Pfad im Template? (th:href="@{/css/star-rating.css}")
  3. Browser-Cache geleert? (Strg+Shift+R)

„Der Hover-Effekt geht nicht“

Prüfe:

  1. Sind die Radio-Buttons in der richtigen Reihenfolge? (5→4→3→2→1)
  2. Hat .star-rating wirklich flex-direction: row-reverse?

„Der Controller bekommt null“

Prüfe:

  1. Hat das Formular method="post"?
  2. Stimmt der name im Fragment mit @RequestParam überein?
  3. Ist mindestens ein Stern ausgewählt?

💬 Geschafft! 🎉

Was du heute gelernt hast:

✅ CSS-only Sternchenbewertung ohne externe Libraries
✅ Den row-reverse Trick für Hover-Effekte
✅ Wiederverwendbare Thymeleaf-Fragments
✅ Controller-Anbindung mit Post-Redirect-Get

Egal ob du das erste Mal von Pseudo-Elementen gehört hast oder dein bestehendes Fragment verbessert hast – du hast etwas Neues gelernt. Das zählt!

Fragen? Schreib uns:

  • Elyndra: elyndra.valen@java-developer.online

Nächstes Thema: Persistenz – Bewertungen in der Datenbank speichern 🚀

Keep learning, keep growing! 💚


Tags: #SpringBoot #Thymeleaf #CSS #StarRating #Tutorial

© 2025 Java Fleet Systems Consulting | java-developer.online

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.