Theme System
The Theme System provides comprehensive theming capabilities for JPortal, including a curated collection of theme presets, dynamic light/dark mode switching, custom font loading, and smooth visual transitions. This system manages all visual styling through CSS custom properties that integrate with Tailwind CSS utilities.
For UI component styling patterns, see Styling System. For theme-related UI components themselves, see Theme & Navigation Components.
Architecture Overview
The theme system operates through multiple layers: theme presets defined as TypeScript objects, a Zustand store for state management, a React context provider for theme operations, CSS custom properties for runtime styling, and dynamic font loading for Google Fonts.
Theme System Architecture

Sources: jportal/src/utils/theme-presets.ts1-1348 jportal/src/components/theme-provider.tsx (referenced), jportal/src/stores/theme-store.ts (referenced), jportal/src/components/theme-selector-dialog.tsx1-195 jportal/src/index.css1-240
Theme Presets Configuration
Theme presets are defined in a central configuration file as a Record<string, ThemePreset> object. Each preset contains a label, optional creation date, and complete style definitions for both light and dark modes.
Theme Preset Structure

Preset Inventory
| Preset Key | Label | Features |
|---|---|---|
adefault |
Default | System default, blue primary, neutral grays |
modern-minimal |
Modern Minimal | Clean whites/blacks, Inter font family |
violet-bloom |
Violet Bloom | Purple primary (#7033ff), Plus Jakarta Sans |
t3-chat |
T3 Chat | Pink/purple tones, custom shadow settings |
twitter |
Twitter blue (#1e9df1), rounded corners (1.3rem) | |
mocha-mousse |
Mocha Mousse | Warm browns/tans, DM Sans font |
bubblegum |
Bubblegum | Pink/cyan pastels, hard shadows (3px offset) |
amethyst-haze |
Amethyst Haze | Purple/pink palette, Geist font |
notebook |
Notebook | Paper-like, Architects Daughter handwriting font |
doom-64 |
Doom 64 | Dark retro gaming, Oxanium font, 0px radius |
catppuccin |
Catppuccin | Popular community theme, Montserrat font |
graphite |
Graphite | Monochrome grays, minimal styling |
perpetuity |
Perpetuity | Teal/cyan focus colors |
Each preset defines 40+ CSS properties for light mode and 35+ for dark mode, including comprehensive color tokens, typography settings, and shadow configurations.
Sources: jportal/src/utils/theme-presets.ts4-1348
Color Token System
All presets implement a standardized token structure derived from Shadcn design system:
| Token Category | Properties | Usage |
|---|---|---|
| Base | background, foreground |
Page background and primary text |
| Surface | card, card-foreground, popover, popover-foreground |
Container surfaces |
| Interactive | primary, primary-foreground, secondary, secondary-foreground |
Buttons, links, active states |
| Semantic | muted, muted-foreground, accent, accent-foreground, destructive, destructive-foreground |
Contextual elements |
| Inputs | border, input, ring |
Form elements and focus rings |
| Charts | chart-1 through chart-5 |
Data visualization colors |
| Sidebar | sidebar-* (8 properties) |
Sidebar-specific tokens |
| Custom | grade-*, marks-* |
JPortal-specific grade colors |
Sources: jportal/src/index.css7-88 jportal/src/utils/theme-presets.ts8-83
State Management with Zustand
Theme state is managed globally using a Zustand store (useThemeStore) that persists the current preset selection and merged style configurations.
Theme Store Data Flow

Store Interface:
themeState: Contains currentpresetkey and completestylesobject with both light and dark variantssetThemeState(newState): Updates the theme state and triggers all subscribers
Theme Selection Process (jportal/src/components/theme-selector.tsx13-25):
- User clicks a theme button in
ThemeSelector handleThemeSelect(presetKey)is called- Preset is retrieved from
defaultPresets[presetKey] - Store is updated with new preset key and deep-copied style objects
- All subscribed components re-render with new theme values
Sources: jportal/src/components/theme-selector.tsx1-69 jportal/src/components/theme-selector-dialog.tsx14-41 jportal/src/stores/theme-store.ts (referenced)
CSS Architecture
The theme system uses a two-layer CSS architecture: CSS custom properties on :root provide runtime values, while Tailwind CSS utilities consume these properties through the @theme inline block.
CSS Variable Flow

Default Values (jportal/src/index.css91-160):
The :root selector defines fallback values for all theme properties. These serve as the initial theme before JavaScript loads and ensure graceful degradation.
Tailwind Integration (jportal/src/index.css7-88):
The @theme inline block maps CSS custom properties to Tailwind's design token system:
--color-*tokens map to Tailwind color utilities--radius-*tokens define border radius scales--shadow-*tokens construct box-shadow values dynamically--font-*tokens define font family stacks
Dynamic Shadow System (jportal/src/index.css65-72): Shadows are composed from multiple CSS variables:
--shadow: var(--shadow-offset-x, 0)
var(--shadow-offset-y, 2px)
var(--shadow-blur, 6px)
var(--shadow-spread, 0)
color-mix(in srgb, var(--shadow-color, #000000)
calc(var(--shadow-opacity, 0.1) * 100%), transparent)
This allows themes to customize shadow appearance through component variables.
Sources: jportal/src/index.css1-240
Theme Provider and Context
The ThemeProvider component wraps the application and provides theme-related functionality through React Context, including the current mode (light/dark) and the toggleTheme function.
Theme Provider Responsibilities

Mode Detection Logic:
- Check
localStorage.getItem('theme-mode')for saved preference - Fall back to
window.matchMedia('(prefers-color-scheme: dark)')for system preference - Default to
'dark'if no preference found
CSS Variable Application:
The provider subscribes to theme state changes and applies all style properties as CSS custom properties on document.documentElement.style. Properties are converted from hex/rgb to HSL format where necessary.
View Transition API (jportal/src/index.css163-189): When toggling modes, the provider uses the View Transition API (if supported) to create a circular reveal animation:
::view-transition-new(root) {
clip-path: circle(0 at var(--x, 50%) var(--y, 50%));
animation-name: expand;
}
The {x, y} coordinates from the click event position the animation origin.
Sources: jportal/src/components/theme-provider.tsx (referenced), jportal/src/index.css163-189
User Interface Components
Three primary components handle user interaction with the theme system: ThemeSelectorDialog, ThemeSelector, and ThemeToggle.
Theme UI Component Hierarchy

ThemeSelectorDialog (jportal/src/components/theme-selector-dialog.tsx18-195):
- Button Gestures:
- Click: Opens theme selection dialog
- Hold (500ms): Toggles light/dark mode at click coordinates
- Swipe horizontal: Cycles to next/previous theme preset
- Swipe vertical: Cycles to next/previous theme preset
- Gesture Implementation: Uses refs to track touch/mouse state and prevent dialog opening after gesture actions (jportal/src/components/theme-selector-dialog.tsx21-143)
- Theme Cycling: Maintains sorted array of theme keys for predictable navigation
ThemeSelector (jportal/src/components/theme-selector.tsx10-69):
- Renders grid of theme buttons (2 cols mobile, 3 tablet, 4 desktop)
- Each button displays 4-color preview stripe from preset's light mode colors
- Active preset highlighted with
variant="default"and reduced opacity - Sorted alphabetically by preset label for consistent ordering
ThemeToggle (jportal/src/components/theme-toggle.tsx8-31):
- Simple icon button showing Sun (light) or Moon (dark)
- Calls
toggleTheme({x, y})with click coordinates - Icon transitions with rotation animations via Tailwind classes
Sources: jportal/src/components/theme-selector-dialog.tsx1-195 jportal/src/components/theme-selector.tsx1-69 jportal/src/components/theme-toggle.tsx1-31 jportal/src/components/Header.jsx1-43
Dynamic Font Loading
The DynamicFontLoader component monitors theme changes and dynamically loads Google Fonts specified in the theme's typography settings.
Font Loading Architecture

Font Family Extraction (jportal/src/utils/fonts.ts3-16):
- Split font stack by comma:
"Plus Jakarta Sans, sans-serif".split(",") - Take first font:
"Plus Jakarta Sans" - Remove quotes and trim whitespace
- Check against system font list (ui-sans-serif, system-ui, etc.)
- Return
nullif system font, otherwise return cleaned name
Font Loading Process (jportal/src/utils/fonts.ts24-35):
buildFontCssUrl(family: string, weights: string[])
→ https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap
loadGoogleFont(family, DEFAULT_FONT_WEIGHTS)
→ Check if already loaded
→ Create <link rel="stylesheet" href="...">
→ Append to document.head
Default Weight Loading (jportal/src/utils/fonts.ts1):
The system requests four standard weights for all fonts: ["400", "500", "600", "700"], balancing coverage with performance.
Pre-loaded Fonts (jportal/index.html8-12): The HTML includes a comprehensive Google Fonts preload covering all theme fonts to avoid flash of unstyled text (FOUT). This includes ~20 font families used across all presets.
Sources: jportal/src/components/DynamicFontLoader.tsx1-34 jportal/src/utils/fonts.ts1-35 jportal/index.html8-12
View Transition Animations
The theme system leverages the View Transition API to create smooth, animated transitions when toggling between light and dark modes.
Animation Configuration (jportal/src/index.css163-189):
@supports (view-transition-name: none) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 1s;
}
::view-transition-new(root) {
clip-path: circle(0 at var(--x, 50%) var(--y, 50%));
animation-name: expand;
}
::view-transition-old(root) {
animation-name: shrink;
}
@keyframes expand {
to {
clip-path: circle(100vmax at var(--x, 50%) var(--y, 50%));
}
}
@keyframes shrink {
to {
clip-path: circle(0 at var(--x, 50%) var(--y, 50%));
}
}
}
Transition Trigger:
When toggleTheme({x, y}) is called:
- CSS variables
--xand--yare set on the root element to the click coordinates document.startViewTransition(() => { /* apply new mode */ })is invoked- Browser captures current state snapshot
- New mode is applied (class toggle on root)
- Browser animates from old to new state using circular reveal
- Animation originates from the click position, creating a natural "spreading" effect
Fallback Behavior: On browsers without View Transition API support, the mode switch is instant with no animation, ensuring functionality is preserved.
Sources: jportal/src/index.css163-189 jportal/src/components/theme-provider.tsx (referenced)
Integration with Application
The theme system integrates with the broader application through strategic placement in the component hierarchy and consistent use of Tailwind utility classes.
Application Integration Points

Component Usage Pattern: All components use semantic Tailwind classes that reference theme tokens:
- Backgrounds:
bg-background,bg-card,bg-muted,bg-primary - Text:
text-foreground,text-muted-foreground,text-primary-foreground - Borders:
border-border,border-input - Accents:
bg-accent,text-accent-foreground
Example from Navbar (jportal/src/components/Navbar.jsx18-46):
<div className="bg-muted"> {/* Uses --color-muted */}
<item.IconComponent
className={`${isActive ? "fill-primary-foreground" : "fill-muted-foreground"}`}
/> {/* Responds to theme colors */}
</div>
Custom JPortal Colors (jportal/src/index.css74-88): The system extends standard Shadcn tokens with JPortal-specific grade and marks colors:
- Grade colors:
--grade-aathrough--grade-f(8 levels) - Marks colors:
--marks-outstanding,--marks-good,--marks-average,--marks-poor
These are used in feature modules for data visualization with consistent theming.
Sources: jportal/src/components/Header.jsx1-43 jportal/src/components/Navbar.jsx1-51 jportal/src/index.css74-88 jportal/src/components/Profile.jsx1-157