Shadcn Profile Page in React: Follow, Tabs und Edit-Sheet
Eine shadcn Profile Page sieht einfach aus, bis du eine baust: ein Cover, ein Avatar, ein Name, ein Follow-Button und ein paar Tabs. Dann kommen die Details. Follow muss sich sofort anfühlen und trotzdem eine fehlgeschlagene Anfrage überstehen. Der Avatar braucht einen Status-Punkt, der auf dem Cover lesbar bleibt, die Zähler müssen zum Button passen, die Tabs brauchen Tastaturbedienung und auf dem Smartphone eine Wischgeste, und der Besitzer muss die Seite bearbeiten können, ohne auf einen Einstellungs-Screen geschickt zu werden. Dieser Guide geht jeden Teil einer React User Profile Page mit dem Block Profile Page von Wingo UI durch. Wir bauen Wingo UI, sind also nicht neutral, aber jedes Muster hier funktioniert auch mit einfachen Teilen aus shadcn/ui.
Was gehört zu einem guten User Profile Page Design?
Ein gutes User Profile Page Design beantwortet schnell zwei Fragen: Wer ist das, und was hat diese Person gemacht? Der Header beantwortet die erste, die Tabs die zweite. So ist der Block aufgebaut, von oben nach unten:
- Cover-Band. Ein Foto (
user.cover) oder, ohne Foto, ein Verlauf aususer.coverColor. Das gibt der Seite eine Identität, ohne dass jemand etwas hochladen muss. - Avatar mit Status. Ein Avatar mit 96px, der das Cover überlappt, mit einem Status-Punkt für online, abwesend oder beschäftigt. Bei Personen, die offline sind, steht stattdessen etwas wie „Seen 2 hours ago“ aus
user.lastSeen. - Identität. Name, ein Verifiziert-Häkchen,
@handle, eine einzeilige Headline, die Bio, der Ort, Links, die sich in einem neuen Tab öffnen, und das Beitrittsdatum. - Aktionen. Follow und Message für Besucher, Edit profile für den Besitzer (
isOwner). - Zähler. Followers, Following und Projects. Auf dem Smartphone werden daraus drei Kacheln in einer Reihe.
- Tabs. Overview (angepinnte Projekte, ein Jahr Contributions, letzte Aktivität), Projects (jedes Projekt als verlinkte Karte) und Activity (eine Timeline, nach Tagen gruppiert).
Probier es unten aus. Folge der Person und schau zu, wie die Zahl der Follower mitzieht. Wechsle die Tabs mit den Pfeiltasten, und fahr mit der Maus über einen Tag in der Heatmap oder fokussier ihn, um seine Anzahl zu sehen.
$ npx wingo-ui@latest add profile-pageWas unterscheidet eine Profilseite von der Einstellungsseite?
Die Profilseite ist öffentlich, die Einstellungsseite privat. Das klingt selbstverständlich, und doch sind viele „Profil“-Vorlagen eigentlich Screens für Kontoeinstellungen. Der Block Profile 1 von ReUI zum Beispiel ist eine vertikale Tab-Leiste mit den Panels Profile, Workspace, Team und Billing und einem Data Grid mit den Teammitgliedern (reui.io (opens in a new tab), geprüft am 6. November 2026). Das ist eine gute Einstellungsseite, nur eben ein anderer Screen.
Wir ziehen die Grenze danach, wer ein Feld sieht:
Der Besitzer bearbeitet also das, was andere sehen, genau dort, wo andere es sehen: in einem Sheet über dem Profil. Für alles andere geht er auf /settings. Unser Block Settings Page deckt diese private Seite ab (Profil, Konto, Benachrichtigungen, Abrechnung, Sicherheit und eine Danger Zone mit Save Bar), und das Tutorial zur React Settings Page erklärt, wie dort gespeichert wird.
Wie installiere ich die Profile Page und binde sie an Next.js an?
Eine shadcn User Profile Page aus statischem Markup überlässt dir den Follow-State, die Validierung und das Speichern. Der Block bringt dieses Verhalten mit, also installierst du ihn, statt ein React Profile Page Template abzuschreiben. Die CLI fügt die Teile hinzu, auf denen er aufbaut (Avatar Upload, Swipeable Tabs, Sheet, Timeline, die Heatmap), samt ihren npm-Paketen:
ProfilePage ist eine Client-Komponente, die Daten als Props bekommt und über Callbacks meldet, was die Person getan hat. Lade das Profil in einer Server-Komponente, entscheide dort, ob es dem Betrachter gehört, und reich die Daten an einen kleinen Client-Wrapper weiter:
Der Wrapper verbindet die Callbacks mit deiner API. onFollow und onSave dürfen beide ein Promise zurückgeben: Follow schaltet zuerst um und rollt bei einer Ablehnung zurück. Save zeigt seinen Spinner, solange das Promise läuft, und bei einer Ablehnung deren Meldung im Sheet.
Die letzten beiden Zeilen von onSave sind wichtiger, als sie aussehen. Der Handle ist Teil der URL. Ohne sie würde ein umbenannter User beim Neuladen auf der alten Adresse in einem 404 landen.
user braucht id, name, handle, followers und following. Alles andere (Avatar, Cover, Headline, Bio, Links, Status, verified) ist optional und wird einfach ausgeblendet, wenn es fehlt. Ganz ohne Props rendert der Block ein Beispielprofil, Emma Carter aus Brooklyn. Das ist praktisch, solange deine API noch nicht fertig ist.
Wie sollen sich der Follow-Button und die Zähler verhalten?
Optimistisch. Sobald du Follow drückst, wird der Button zu Following, ein Häkchen erscheint, der Zähler Followers steigt um eins, und Screenreader hören „You follow Emma Carter“. Erst dann wartet der Block auf onFollow(true). Wird das Promise abgelehnt, schaltet der Button zurück, und Screenreader hören „That did not work. Try again.“ Ein zweiter Tipp, während eine Anfrage läuft, wird ignoriert. So schickt ein Doppeltipp nie Follow und Unfollow gleichzeitig los.
Zwei Details halten den Zähler ehrlich. Wenn deine Daten schon sagen, dass der Betrachter dieser Person folgt (user.isFollowing), enthält die übergebene Follower-Zahl ihn bereits, und der Block zählt nicht noch einmal eins dazu. Und wenn du den Follow-State woanders verwaltest (in einem Store oder einem Query-Cache), übergib following und kümmere dich selbst um onFollow. Der Block rendert dann nur, was du ihm gibst.
Der Zähler Projects öffnet immer den Tab Projects. Followers und Following werden zu Buttons, sobald du onStatClick übergibst. Der Callback bekommt "followers" oder "following", also kannst du eine Liste in einem Sheet öffnen oder auf /u/emma/followers weiterleiten.
Wie funktionieren die Swipeable Tabs und die Heatmap?
Die Tabs sind Swipeable Tabs in der Variante underline. Auf dem Desktop folgen sie dem WAI-ARIA-Muster für Tabs: Die Pfeiltasten wechseln zwischen den Tabs, Pos1 und Ende springen zum ersten und zum letzten (W3C APG tabs pattern (opens in a new tab)). Unter 768px folgen die Tab-Seiten außerdem einer horizontalen Wischgeste, wie auf einem nativen Profil-Screen, während vertikales Scrollen weiter die Seite bewegt. Enthält ein Tab ein horizontales Karussell oder eine Karte, die sich mit der Geste in die Quere käme, setz data-no-swipe auf dieses Element. Dann bleiben horizontale Drags, die dort beginnen, bei ihm. Oder übergib swipeable={false}, um das Wischen ganz abzuschalten. Mehr zur Geste steht in React Swipeable Tabs.
Die Heatmap im Tab Overview ist GitHub Activity, und die ist kostenlos. Sie bekommt ein Jahr an Tagen, jeder als { date, count, level } mit level von 0 bis 4, beginnend an einem Sonntag. Berechne das Level auf dem Server, damit der Client nur noch Zellen zeichnet:
Das sind dieselben Schwellen, die auch die Beispieldaten nutzen. Sind deine User viel häufiger aktiv, heb sie an, sonst färbt sich das ganze Jahr im dunkelsten Grün. heatmapColor nimmt jede CSS-Farbe (aktivere Tage sind deckender), und showHeatmap={false} oder ein leeres Array blendet das Raster aus. Für die Komponente allein, mit einem GitHub-Usernamen statt deiner eigenen Daten, lies den Guide zum React GitHub Contribution Graph.
Wie validiert und speichert das Sheet zum Bearbeiten des Profils?
Auf der eigenen Seite öffnet Edit profile ein Sheet: auf dem Desktop von rechts, auf dem Smartphone als Bottom Sheet mit Griff zum Ziehen. Darin stehen Foto, Name, Username, eine Bio mit Zeichenzähler bis 160, Ort und Website. Das Fotofeld ist Avatar Upload, der Besitzer kann also zuschneiden, den Upload-Fortschritt verfolgen oder das Bild entfernen. Übergib onAvatarUpload, um den zugeschnittenen Blob an deinen Speicher zu schicken und die neue URL zurückzugeben. Ohne den Callback bleibt das Foto bis Save eine lokale Vorschau.
Öffne unten das Sheet und drück Save mit leerem Namen. Tipp dann einen Username mit Leerzeichen ein.
$ npx wingo-ui@latest add profile-pageSave prüft drei Dinge, bevor es onSave aufruft: Der Name ist nicht leer, der Username besteht aus 3 bis 20 Buchstaben, Ziffern oder Unterstrichen (ein führendes @ wird entfernt), und die Website sieht wie eine Adresse aus. Das erste ungültige Feld bekommt den Fokus, und nach diesem ersten Versuch aktualisieren sich die Meldungen beim Tippen. Gültige Werte kommen getrimmt an, und ein nacktes example.com bekommt https:// vorangestellt. Solange das Promise läuft, zeigt Save einen Spinner, und das Sheet lässt sich nicht schließen. Escape kann also keinen halb gespeicherten Entwurf verwerfen. Eine Ablehnung behält den Entwurf und zeigt die Meldung des Fehlers im Sheet. Ein Erfolg aktualisiert sofort den Header und schließt das Sheet.
Validier trotzdem auf dem Server. Die Prüfung im Client spart nur einen Roundtrip, und ein Username kann zwischen dem Öffnen des Sheets und dem Druck auf Save vergeben werden. Gib also eine klare Fehlermeldung zurück, dann zeigt das Sheet sie an.
Was passiert auf dem Smartphone?
Das Layout ist Mobile First. Unter 640px stapelt sich der Header: Der Avatar überlappt das Cover, die Zähler werden zu drei gleich breiten Kacheln, und Follow und Message rutschen ans Ende des Headers und teilen sich die volle Breite. Die Tab-Seiten lassen sich wischen, und das Edit-Sheet wird zum Bottom Sheet mit Cancel und Save in voller Breite. Die Tastatur bleibt dabei unten, bis du ein Feld antippst. Um ein brandneues Konto zu prüfen, übergib projects={[]} mit defaultTab="projects": Der Empty State zeigt „No public projects yet“ statt eines leeren Rasters.
Worauf sollte ich achten?
In Produktion gehen vier Dinge öfter schief als das Layout.
- Private Daten geraten nach außen. Das
user-Objekt landet im Browser. Bilde deine Datenbankzeile auf dem Server aufProfileUserab und lass E-Mail, Telefon und interne Flags weg. - Veraltete Zähler nach dem Speichern. Der Header zeigt die gespeicherten Werte sofort. Übergibt deine Elternkomponente später ein neues
user-Objekt, ersetzt es diese Werte. Lade die Route nach einem erfolgreichen Speichern neu oder revalidiere sie (dasrouter.refresh()oben), damit beides übereinstimmt. - Hydration und Uhrzeit. Relative Zeiten wie „Updated 5 hours ago“ lesen die echte Uhr erst nach der Hydration. Das Server-HTML lässt sie deshalb weg, so entsteht kein Hydration-Mismatch. Übergib
now, wenn du sie schon im Server-HTML brauchst, und in Tests und Screenshots, damit sie nie abdriften. - Übersetzungen. Jeder sichtbare Text steht in
labels, das überDEFAULT_PROFILE_PAGE_LABELSgemergt wird, undlocaleformatiert Zähler und Datumsangaben (1,284 inen-US, 1.284 inde-DE).
Wo es weitergeht
Wenn du den Rest der App um diesen Screen herum baust, sammelt die Kategorie Praxis: Screens und Apps unsere anderen Guides zu ganzen Seiten. Der Guide zum SaaS Website Template behandelt die öffentlichen Seiten, die daneben stehen. Für eine Portfolio-Website statt eines Profils in der App passt das Next.js Portfolio Template besser.
Die Blöcke Profile Page und Settings Page gehören zu Wingo UI Pro (Pläne und Preise). Heatmap, Avatar, Sheet und die Formularfelder sind kostenlos: Leg einen kostenlosen Account an, führ npx wingo-ui@latest add github-activity avatar sheet aus und starte von dort.
Häufige Fragen
Was gehört auf eine Profilseite?
Ein Cover oder ein farbiges Band, der Avatar, der Name mit Handle, eine kurze Bio, Ort und Links, die wichtigsten Aktionen (Follow und Message, auf der eigenen Seite Edit profile), ein paar Zähler und die Arbeit der Person in Tabs. Alles, was nur der Besitzer sehen soll, etwa die E-Mail-Adresse, bleibt von der öffentlichen Seite fern.
Was unterscheidet eine Profilseite von einer Einstellungsseite?
Die Profilseite zeigt eine Person allen und lässt den Besitzer die öffentlichen Felder direkt dort bearbeiten. Die Einstellungsseite ist privat: E-Mail, Passwort, Zwei-Faktor, Benachrichtigungen, Abrechnung und das Löschen des Kontos. Wenn Edit profile ein Sheet öffnet und Settings eine eigene Route hat, verschwimmen die beiden nicht.
Sollte der Follow-Button auf den Server warten?
Nein. Schalte ihn sofort um, zähl einen Follower dazu, schick dann die Anfrage und schalte ihn mit einer Meldung zurück, wenn sie fehlschlägt. Wer bei jedem Tipp ein paar hundert Millisekunden wartet, hält den Button bei einer langsamen mobilen Verbindung für kaputt.
Gibt es ein kostenloses shadcn Profile Page Template?
Der Block Profile Page von Wingo UI gehört zu Pro, ebenso die Teile Avatar Upload, Swipeable Tabs und Timeline, die er nutzt. Die Heatmap GitHub Activity, Avatar, Sheet, Button, Card, Badge, Input und Textarea sind mit einem kostenlosen Account frei, daraus kannst du ein einfacheres Profil zusammenbauen.
Wie zeige ich eine Contributions-Heatmap in einem React-Profil?
Gib der Heatmap ein Jahr an Tagen im Format { date, count, level } mit level von 0 bis 4, beginnend an einem Sonntag. Berechne das Level auf dem Server aus deiner eigenen Aktivitätstabelle, dann rendert der Client nur noch Zellen.
- Profil
- shadcn
- React
- Next.js
- Mobile UI

