projects / easemysemester / features
shadcn/ui migration with an animated dark mode switch
EaseMySemester's admin and student UI was rebuilt on shadcn/ui with a semantic color token system and a self-built animated light/dark switch.
feature · current · updated 2026-09-15 · EaseMySemester case study
EaseMySemester's UI ran on plain CSS with no component system and no dark mode. See overview. I migrated the whole admin and student surface to shadcn/ui on Tailwind CSS v4, in one pass rather than a hybrid rollout, since the surface was only about 15 files.
What changed
- A semantic color token system (primary, muted, success, warning, destructive, info) replaces arbitrary hex values, with separate light and dark HSL ramps mapped through Tailwind v4's
@theme inlineblock. - Dashboard, browse, question lists, forms, and tables were regrouped for visual hierarchy: stat cards, grouped panels, breadcrumbs, and questions grouped by type with paper and occurrence badges.
sonnertoasts handle transient success/error feedback (import, publish, signup); inlineAlertcovers page-level notices.- Existing functionality (import, publish, invite, auth, browse) was left unchanged. This was a UI layer change only.
The dark mode toggle
The first build used a dropdown menu (Light / Dark / System). I replaced it with a single animated switch: a pill track with a sliding thumb that carries a Sun or Moon icon, and dropped "System" as a selectable option (the app still follows the OS preference on first load, via next-themes).
The first build of the switch was misaligned: icons floated on top of the track instead of inside the thumb, and disableTransitionOnChange on the theme provider was silently cancelling the slide animation by force-disabling all CSS transitions during a theme swap. The fix was a self-contained <button role="switch"> (no Radix dependency) with the thumb driven by resolvedTheme, plus removing disableTransitionOnChange from the app layout.
Why this shape
A hand-rolled switch gave full control over track and thumb geometry, avoiding a cascade fight between the thumb's own transform and any parent override. Keeping defaultTheme="system" and enableSystem on the provider preserves first-load OS detection while the visible control stays a simple binary toggle.
Status
Both specs are built and verified: an 18-route build passes in light and dark, and the toggle animates correctly with npx tsc --noEmit and next build both green.