biswarupmondal.com
Design System
The visual language, component patterns, and design principles behind this portfolio. A living reference that documents what's already here — and guides what comes next.
Colors
Semantic tokens with light/dark variants. Click any swatch to copy its CSS variable name.
Core
Structural surfaces and text. --color-bg always pairs with --color-text to maintain contrast in both themes. Use --color-text-secondary only for supporting copy, never for primary labels.
Accent
Interactive elements exclusively — buttons, links, focus rings, active states. Never use accent for decoration or to highlight non-interactive content.
Feedback
Strictly semantic. Success for positive outcomes only; Error for failures and destructive actions. Never swap them or repurpose for non-state uses.
Taskbar
OS taskbar chrome only. Do not use these tokens inside windows, cards, or content areas — they are scoped to the navigation layer.
Typography
Two font stacks: Google Sans for display, system-ui for UI. Click any row to copy.
Hierarchy Reference
| Token | Size | UI Role | Font | Weight |
|---|---|---|---|---|
| --font-size-3xl | 2rem | Page titles, hero headlines | Google Sans | 700 |
| --font-size-2xl | 1.5rem | Section headings | Google Sans | 700 |
| --font-size-xl | 1.25rem | Sub-headings, window titles | System UI | 600 |
| --font-size-lg | 1.125rem | Lead paragraphs, callouts | System UI | 400 |
| --font-size-base | 1rem | Body text, paragraphs | System UI | 400 |
| --font-size-sm | 0.875rem | Captions, labels, metadata | System UI | 400–500 |
Spacing
A 6-step scale from 4px to 48px. Click any row to copy the CSS variable.
Motion
200ms / ease-out baseline. Overridden to 0ms when prefers-reduced-motion is set.
| Token | Value | Notes |
|---|---|---|
| --motion-duration | 200ms | Standard transition speed. Fast enough to feel snappy, slow enough to register. |
| --motion-easing | ease-out | Decelerating easing. Objects decelerate to rest — matches physical world intuition. |
Hover to see the motion tokens in action
transform 200ms ease-out · respects prefers-reduced-motion
Elevation
Shadow tokens map to the OS window hierarchy — resting, focused, dropdown, and taskbar.
Z-Index
Three explicit layers model the OS stacking context.
Tooltip
Hover/focus tooltip with smart auto-positioning and keyboard shortcut support.
A wrapper component that shows a tooltip on hover or focus. Supports smart auto-positioning, keyboard shortcuts, multiline content, and reduced-motion via Framer Motion's AnimatePresence.
src/components/ui/Tooltip.tsxVariants
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| content | string | — | Text to display in the tooltip (required) |
| position | 'top' | 'bottom' | 'left' | 'right' | 'top' | Preferred position. Overridden by autoPosition when near viewport edges |
| delay | number | 200 | Milliseconds before the tooltip appears on hover |
| disabled | boolean | false | Prevents the tooltip from showing |
| keyboardShortcut | string | — | Appended to tooltip text in parentheses — e.g. ⌘K |
| autoPosition | boolean | true | Recalculates position to avoid viewport clipping |
| multiline | boolean | false | Allows tooltip text to wrap for longer content |
Accessibility
- Tooltip element has
role="tooltip"andaria-live="polite" - Shows on both hover and keyboard focus — fully keyboard accessible
- Escape key dismisses the tooltip and returns focus to the trigger element
- Animation collapses to zero duration when
prefers-reduced-motionis set
Do's & Don'ts
Image Lightbox
Portal-rendered full-screen image overlay with gallery navigation.
A full-screen image overlay rendered via a React portal directly on document.body. Supports single images and multi-image galleries with prev/next navigation and an image counter.
src/components/ui/ImageLightbox.tsxVariants
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| isOpen | boolean | — | Controls whether the lightbox is visible (required) |
| imageSrc | string | null | — | URL of the image to display (required) |
| alt | string | — | Accessible alt text for the image (required) |
| onClose | () => void | — | Called when user closes the lightbox (required) |
| onNext | () => void | — | Called when user navigates to next image. Renders next button when provided |
| onPrevious | () => void | — | Called when user navigates to previous image. Renders prev button when provided |
| currentIndex | number | — | Zero-based index of the current image for the counter display |
| totalImages | number | — | Total number of images — counter shows when this is > 1 |
Accessibility
- Escape key closes the lightbox; ArrowLeft/ArrowRight navigate between images
- Locks
body.style.overflowtohiddenwhile open — prevents background scroll - All controls (close, previous, next) have explicit
aria-labelattributes - Rendered via
createPortalondocument.body— avoids z-index stacking context issues - Clicking the overlay backdrop closes the lightbox
Do's & Don'ts
Tabs
Horizontal tab bar for switching between content sections within a window.
A horizontal tab bar used inside case study windows to switch between content sections. Follows the ARIA tablist/tab pattern with aria-selected for screen reader state.
src/components/windows/SystemTabs.tsxVariants
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| activeTab | 'Overview' | 'Decisions' | 'Principles' | 'Changes' | 'Issues' | — | The currently active tab (required, controlled) |
| onTabChange | (tab: TabType) => void | — | Callback fired when the user selects a different tab (required) |
Accessibility
- Tab container has
role="tablist"; each button hasrole="tab" aria-selected="true"is set on the active tab;aria-labelon each tab button- Active indicator is a 2px border-bottom line — never communicated by color alone
- Each tab has an
aria-labelappending "tab" (e.g. "Overview tab")
Do's & Don'ts
Window Chrome
OS-style window frame with traffic-light controls and shadow elevation states.
The OS-style window chrome used for all floating secondary windows. Provides the title bar, traffic-light controls (close/minimize/maximize), and consistent border-radius + shadow elevation. Supports both resting and focused shadow states.
src/components/windows/SecondaryWindow.tsxVariants
--window-shadow-focused — a stronger shadow that visually brings it forward and signals user attention.Key Tokens
| Prop | Type | Default | Description |
|---|---|---|---|
| --window-shadow | shadow token | 0 4px 12px rgba(0,0,0,0.1) | Resting window elevation |
| --window-shadow-focused | shadow token | 0 8px 24px rgba(0,0,0,0.15) | Active / focused window — stronger shadow |
| --window-border-radius | CSS var | 8px | Consistent corner radius for all windows |
Accessibility
- Traffic light buttons should have
aria-labelvalues: "Close", "Minimize", "Maximize" - Draggable title bar region should be identified — avoid placing interactive controls in the drag zone
- Window titles are rendered in the title bar for screen reader context
- Focused window state is indicated by shadow change — never by color alone
Do's & Don'ts
Card
Bordered surface with hover elevation — used for project and metric display.
A bordered surface for grouping related content — used for project thumbnails, metric highlights, and case study entry points. On hover, the card lifts with a shadow and a subtle upward translation.
Pattern — work cards, playground cards, metric cardsVariants
Case Study Title
A brief description of the project, the problem it solved, and the impact it had on users.
+38%
Conversion lift
Metric card variant for impact numbers.
Hover State Tokens
| Prop | Type | Default | Description |
|---|---|---|---|
| box-shadow | CSS property | none | Resting state — uses var(--color-border) for the outline border |
| box-shadow (hover) | CSS property | var(--window-shadow) | Elevated state — uses window shadow token on hover |
| transform (hover) | CSS property | translateY(-2px) | Subtle lift — 2px upward translation on hover |
| transition | CSS property | 200ms ease-out | Uses --motion-duration and --motion-easing tokens |
Accessibility
- If the entire card is clickable, wrap in an
<a>element — do not nest buttons inside - The hover shadow change is purely visual — content within the card provides the semantic structure
- Interactive cards need a visible focus ring — inherited from the
<a>or<button>wrapper
Do's & Don'ts
Form Elements
Input, Select, and Textarea with shared focus ring pattern.
Form input patterns used in the contact form. All inputs use the same focus ring pattern (--color-focus border + --color-focus-ring glow) to give consistent keyboard feedback across input types.
src/components/windows/ContactEmailForm.tsxVariants
Focus Ring Tokens
| Prop | Type | Default | Description |
|---|---|---|---|
| border-color | CSS property | var(--color-border) | Default border — changes to --color-focus on :focus |
| box-shadow | CSS property | none | On :focus: 0 0 0 3px var(--color-focus-ring) — visible ring glow |
| --color-focus | token | #6366f1 / #818cf8 | Focus border color — matches accent in light and dark |
| --color-focus-ring | token | rgba(99,102,241,0.3) | Semi-transparent glow around focused inputs |
Accessibility
- Every input must have an associated
<label>viahtmlFor/id— placeholder is not a substitute - Required fields should use both
requiredattribute andaria-required="true" - Focus ring meets WCAG 2.1 SC 2.4.11 (Enhanced Focus Appearance) minimum of 2px and 3:1 contrast ratio
- Group related fields with
<fieldset>and<legend>for screen reader context
Do's & Don'ts
Sticky Note
Draggable, editable desktop note with localStorage persistence.
A draggable, editable sticky note pinned to the desktop. Content and position persist in localStorage. Designed for jotting quick UX decisions without leaving the desktop context — behaves like a real physical note.
src/components/desktop/StickyNote.tsxVariants
Empty
With content
Dragging
Behaviour
| Prop | Type | Default | Description |
|---|---|---|---|
| content persistence | localStorage | sticky-note-content | Text survives full page reloads. Content versioning key prevents stale data from old schema. |
| position persistence | localStorage | sticky-note-position | Stores { x, y } pair. Resets to default corner position if no stored value found. |
| drag target | header only | — | Dragging is only activated on the note header — the content area allows text selection freely. |
| default position (desktop) | CSS | right: --spacing-md, top: 52px | Appears in top-right, just below the taskbar. |
| default position (mobile) | CSS | right: --spacing-sm, bottom: calc(32px + --spacing-sm) | Anchored above the system status bar on small screens. |
Accessibility
- Content area has
role="textbox",aria-multiline="true", andaria-label="Sticky note with recent UX decisions" - Drag events are mouse and touch — both pointer types supported
- No keyboard drag support; position resets are handled via localStorage clear
AQI Card
Live air quality index widget fetching real-time data via the /api/air-quality endpoint.
A live air quality index widget pinned to the desktop. Fetches real-time AQI data from /api/air-quality using GPS or IP-derived coordinates, refetches every 10 minutes, and renders into a portal so it floats above all content.
src/components/desktop/AqiCard.tsxVariants
Loading
Good (0–50)
Moderate (51–100)
Unhealthy (101–150)
Very Unhealthy (151–200)
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| locationData | LocationData | required | IP-based location fallback: { ip, state, country, latitude, longitude } |
| gpsCoords | { lat: number; lng: number } | null | null | Precise GPS coordinates. When available, preferred over IP-based coords for the API call. |
AQI Scale
| Prop | Type | Default | Description |
|---|---|---|---|
| 0–50 | Good | Green | Air quality is satisfactory; little to no risk. |
| 51–100 | Moderate | Yellow | Acceptable; unusually sensitive people may be affected. |
| 101–150 | Unhealthy (sensitive) | Orange | Sensitive groups may experience effects. |
| 151–200 | Unhealthy | Red | Everyone may begin to experience health effects. |
| 201–300 | Very Unhealthy | Purple | Health alert — everyone may experience serious effects. |
States
- Loading skeleton — shown when no GPS/IP coordinates are available yet
- Loading — shown while the API call is in flight
- Error — shown when the API fails; includes a retry hint message
- Data — shows AQI value, label, and key pollutants (PM2.5, Ozone, PM10, CO)
Accessibility
- Container has
role="complementary"andaria-label="Air Quality Index" - Portal renders into
#desktop-cards-root(falls back todocument.body) - Position persisted via
localStoragekeyaqi-card-position
Weather Card
Live weather widget with 9 dynamic themes driven by current conditions.
A live weather widget with dynamic theming. Fetches from /api/weather and applies one of 9 weather themes via a data-weather-theme attribute — the card's background, icon, and text palette all shift with the current conditions.
src/components/desktop/WeatherCard.tsxVariants
clearDay
clearNight
rainy
stormy
snowy
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| locationData | LocationData | required | IP-based location fallback: { ip, state, country, latitude, longitude } |
| gpsCoords | { lat: number; lng: number } | null | null | GPS coordinates preferred over IP coords when available. |
Weather Themes
| Prop | Type | Default | Description |
|---|---|---|---|
| clearDay | data-weather-theme | — | Bright blue gradient; sunny day palette |
| clearNight | data-weather-theme | — | Deep navy gradient; night sky palette |
| cloudyDay | data-weather-theme | — | Muted grey-blue; overcast day palette |
| cloudyNight | data-weather-theme | — | Dark slate; overcast night palette |
| rainy | data-weather-theme | — | Cool blue-grey; rain palette |
| snowy | data-weather-theme | — | Pale blue-white; snow palette |
| stormy | data-weather-theme | — | Dark purple-grey; storm palette |
| foggy | data-weather-theme | — | Muted warm grey; fog palette |
| default | data-weather-theme | fallback | Neutral palette when condition cannot be determined |
Data Displayed
- Temperature (current, feels-like, high/low)
- Weather condition label
- Wind speed and humidity
- Refetches every 10 minutes — no manual refresh needed
Accessibility
- Container has
role="complementary"andaria-label="Weather" - Portal renders into
#desktop-cards-root(falls back todocument.body) - Position persisted via
localStoragekeyweather-card-position
Desktop Icon
Draggable shortcut that opens a window and syncs the browser URL on click.
A draggable, clickable desktop shortcut. Each icon is positioned absolutely on the desktop canvas, opens a specific window on click, and updates the browser URL via pushState. A 5px drag threshold prevents accidental window opens when the user intends to reposition the icon.
src/components/desktop/DesktopIcon.tsxVariants
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | required | Unique identifier for this icon instance |
| iconType | 'work' | 'playground' | 'thoughts' | 'about' | 'contact' | 'resume' | 'trash' | 'folder' | 'image' | 'file' | required | Determines which icon SVG to render |
| label | string | required | Visible text label below the icon |
| windowType | string (12 values) | required | Window type to open on click — passed to openWindowViaEvent() |
| windowTitle | string | required | Title shown in the opened window chrome |
| windowContent | React.ReactNode | required | Content rendered inside the opened window |
| initialX | number | required | Starting X position relative to the desktop container |
| initialY | number | required | Starting Y position (container offset = 42px for taskbar) |
| onPositionChange | (id, x, y) => void | required | Callback fired after drag ends with updated coordinates |
| isSelected | boolean | false | Applies selected visual state (highlight ring) |
| onSelect | () => void | undefined | Fires when icon receives focus or is clicked without dragging |
| onDeselect | () => void | undefined | Fires when the icon loses selection |
Interaction Details
- 5px drag threshold — pointer must move more than 5px before a drag starts; clicks below threshold open the window instead
- URL sync — on click,
window.history.pushStateupdates the URL to the window's route - Origin animation — captures the icon's bounding rect and passes it to
openWindowViaEventfor the macOS-style scale animation - Container-relative positioning — container top offset (42px for taskbar) is subtracted from mouse Y to keep icons in the correct desktop zone
Accessibility
role="button",tabIndex=0,aria-label="Open {label}"- Keyboard: Enter/Space triggers the window open (same path as mouse click)
- No keyboard drag support — position is managed by parent via
onPositionChange
Taskbar
Global top navigation — Astro shell with React islands for interactive nav and mobile dropdown.
The global top navigation bar. An Astro component shell (zero JS) that composes three parts: a logo anchor, a React island for nav buttons (TaskbarButtons), and a React island for the mobile dropdown (MobileNavDropdown). Uses backdrop-filter: blur(20px) for the translucent macOS-style chrome.
src/components/navigation/Taskbar.astrosrc/components/navigation/TaskbarButtons.tsxVariants
Structure
| Prop | Type | Default | Description |
|---|---|---|---|
| Logo anchor | Astro (static) | href="/" | SVG monogram + name/title text. Two SVGs for light/dark (CSS display toggled by data-theme). |
| TaskbarButtons | React island (client:load) | — | Renders nav buttons: Work, Playground, Thoughts, Design System, Contact, About. Each opens a window or external tab. |
| MobileNavDropdown | React island (client:load) | — | Chevron dropdown replacing nav buttons at ≤768px. |
| taskbar-right slot | DOM slot | #taskbar-right-slot | Empty slot for right-side components injected by other React islands (e.g. SystemStatus, ThemeSwitcher). |
Design Tokens
| Prop | Type | Default | Description |
|---|---|---|---|
| --taskbar-text | CSS var | theme-aware | Nav button and logo name text color |
| --taskbar-text-secondary | CSS var | theme-aware | Logo subtitle (role/title) text color |
| --taskbar-shadow | CSS var | subtle drop shadow | Bottom edge shadow — separates bar from desktop content |
| height | hardcoded | 42px | Fixed taskbar height — desktop icon positioning uses this value as top offset |
| backdrop-filter | CSS | blur(20px) saturate(180%) | Translucent glass effect. Light theme: rgba(242,237,234,0.7). Dark theme: rgba(46,45,49,0.7). |
Responsive Behaviour
- ≤768px: Desktop nav buttons hidden (
.desktop-nav-buttons→display:none). MobileNavDropdown shown. - ≤480px: Logo title hidden; logo text reduces to 11px; taskbar height drops to 40px.
- System Status (clock/battery) is hidden on mobile via
.desktop-onlyclass.
Accessibility
<nav role="toolbar" aria-label="Main navigation">wraps the entire bar- Logo link has
aria-label="Home"and SVGs arearia-hidden="true" - Each nav button has
aria-label="Open [Window] window" - Focus ring:
outline: 2px solid var(--color-focus); outline-offset: 2px prefers-reduced-motion: transitions set tononeon logo and buttons
System Status
Live clock and battery indicator in the taskbar's right slot.
A live system status widget in the taskbar's right slot showing the current date/time and device battery level. Rendered as a React island — updates every second for the clock and listens to the Battery Status API for charging state changes.
src/components/navigation/SystemStatus.tsxVariants
Props
No props — fully self-contained. All data is derived from the browser's Date API and the navigator.getBattery() API.
Battery States
| Prop | Type | Default | Description |
|---|---|---|---|
| Charging | state | --color-accent | Battery icon + level shown in accent color; "+" charging indicator appended |
| ≥50% | state | --color-text | Normal battery level — default text color |
| 20–49% | state | #f59e0b (amber) | Low battery warning color |
| <20% | state | #dc2626 (red) | Critical battery level color |
| API unsupported | state | --color-text-secondary | Shows "AC" text + empty battery outline — assumed plugged-in desktop |
Time Format
- Single-line macOS-style:
Wed Mar 5 3:45 PM(weekday + month + day + 12h time) - Full locale timestamp shown in
titletooltip on hover - Clock interval: 1 second via
setInterval— cleared on unmount
Accessibility
- Battery container has a
titleattribute with full battery status text - Battery SVG icon is
aria-hidden="true"— the text label carries the meaning - Hidden on mobile via
.desktop-onlyclass (not rendered in the DOM on small screens)
Design Principles
The ideas that shape every decision in this system.
Desktop as the Interface
The portfolio itself is the design case study. The OS-inspired window system shows — rather than tells — how I think about information architecture, spatial reasoning, and interaction design.
Adaptive by Default
Every color token has a light and dark counterpart. The theme system uses native CSS custom properties — no JavaScript runtime overhead. System preference is respected and overrides are persisted to localStorage.
Motion with Intent
200ms / ease-out is the baseline. Animations are never decorative — they orient the user, confirm actions, and communicate state. All motion respects prefers-reduced-motion at the token level.
Accessible by Construction
Focus rings use --color-focus-ring. Color contrast meets WCAG AA minimums. Keyboard navigation is first-class. Reduced motion is handled at the CSS variable level so it works without JavaScript.
Islands Architecture
React islands only where interactivity is truly needed. Static Astro for everything else. This keeps the core fast — zero JS for content that doesn't need it.
Tokens Over One-Offs
All spacing, color, and motion values reference CSS custom properties. One-off values exist only in component-specific cases (like taskbar height 42px). This keeps the system coherent as it grows.
Patterns
Recurring structural patterns in the codebase.
Content Type Convention
Work, Thoughts, and Playground each map to a distinct window and route pattern.
// Data lives in TypeScript files — no CMS src/lib/data/workProjects.ts // Work case studies src/lib/data/blogPosts.ts // Thoughts / articles src/lib/data/playgroundProjects.ts // Routes are generated via getStaticPaths() /work/[slug] → WorkProject with seo + content[] /thoughts/[slug] → BlogPost with seo + markdown /playground/[slug] → PlaygroundProject
Window System Mental Model
PrimaryWindow (always visible) + SecondaryWindows (floating, draggable, stackable). WindowContext manages state.
// Window registration WindowContext.tsx // Core state (open/close/focus/minimize) PrimaryWindow.astro // Fixed main content area SecondaryWindow.tsx // Draggable floating windows SecondaryWindowLayer.tsx // Portal target for secondary windows // Z-index: primary=1 · secondary=10 · screensaver=1000
Theme Switching
CSS custom properties + data-theme attribute. LocalStorage persists the preference across sessions.
// Three modes: light | dark | system [data-theme="light"] → Light CSS vars [data-theme="dark"] → Dark CSS vars [data-theme="system"] → Follows prefers-color-scheme // Theme is set inline on <html> before first paint // Prevents flash of wrong theme (FOWT)
Responsive Breakpoints
Three breakpoints collapse the two-column layout to single-column mobile from 1280px down to 480px.
/* Desktop (default): sidebar + main content */ grid-template-columns: 220px 1fr @media (max-width: 1024px) → Sidebar narrows to 180px @media (max-width: 768px) → Sidebar hidden, single-column layout → main padding: 24px 20px @media (max-width: 480px) → Compact header (title hidden) → Type scale rows stack vertically
Islands Architecture
React only where interactivity is genuinely needed. Static Astro handles everything else — zero JS cost.
// Static Astro (zero JS shipped) src/layouts/BaseLayout.astro // Page shell, SEO, theme init src/layouts/OnboardingLayout.astro // Lightweight standalone pages src/components/navigation/Taskbar.astro // React islands (client:load — JS only where needed) src/components/WindowApp.tsx // Window system state src/components/design-system/DesignSystemDoc.tsx src/components/windows/EmailComposer.tsx // Rule: if it doesn't need useState or useEffect → use Astro
SEO / Meta Pattern
All SEO, OG tags, Twitter Cards, and JSON-LD live in BaseLayout — never duplicated across pages.
// Every page passes typed props to BaseLayout
{ title, description, ogImage?, keywords?, canonical? }
// JSON-LD schemas injected per context:
Person schema → / (homepage)
Article schema → /thoughts/[slug]
WebPage schema → /design-system, /about, /contact
// Sitemap auto-generates from all page routes
src/pages/sitemap.xml.ts → priority + changefreq per route
// Canonical URLs: Astro.url.href (absolute, no trailing slash)