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:
- 5 Sterne – klassisch, jeder kennt es
- Klickbar – der User kann eine Bewertung abgeben
- Hover-Effekt – visuelle Rückmeldung beim Überfahren
- Kein JavaScript – nur CSS und HTML (ja, das geht!)
- 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

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
| Parameter | Typ | Beschreibung |
|---|---|---|
name | String | Der Name für das Formularfeld. Wichtig für den Controller. |
value | Integer | Der aktuelle Wert (0-5). Bei 0 ist nichts ausgewählt. |
readonly | Boolean | Wenn 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:
- User klickt Stern 4
- Browser sendet POST an
/ratemitrating=4 - Controller validiert, speichert, setzt Flash-Attribute
- Redirect zu
/ - 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 intemplates/fragments/rating.html:: stars(...)→ Nimm das Fragment namens „stars“'rating'→ Der Formularfeld-Name${rating}→ Der aktuelle Wert aus dem Modelfalse→ 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-labelauf den Inputs gibt jedem Stern einen verständlichen Namenaria-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:
| Projekt | Fü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
| Ressource | Beschreibung |
|---|---|
| Thymeleaf Getting Started | Offizielle Einführung |
| CSS Selectors Explained | MDN Doku zu Selektoren |
| Spring Boot Tutorial | Offizieller Quick Start |
🛠️ Offizielle Dokumentation
🛠️ Troubleshooting
„Die Sterne werden nicht angezeigt“
Prüfe:
- Ist
star-rating.cssin/static/css/? - Stimmt der Pfad im Template? (
th:href="@{/css/star-rating.css}") - Browser-Cache geleert? (Strg+Shift+R)
„Der Hover-Effekt geht nicht“
Prüfe:
- Sind die Radio-Buttons in der richtigen Reihenfolge? (5→4→3→2→1)
- Hat
.star-ratingwirklichflex-direction: row-reverse?
„Der Controller bekommt null“
Prüfe:
- Hat das Formular
method="post"? - Stimmt der
nameim Fragment mit@RequestParamüberein? - 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

