biswarupmondal.comDesign System

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.

15Color tokens
6Spacing steps
6Type sizes
14Components

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.

Do
Always pair --color-bg with --color-text — they maintain WCAG AA contrast in both light and dark themes
Use --color-accent for all interactive elements: buttons, links, focus rings, active tab indicators
Use --color-text-secondary for supporting copy (metadata, captions, placeholders)
Don't
Don't use --color-success for non-success states — "Featured" or "New" tags should use --color-accent
Don't mix Taskbar tokens (--taskbar-bg, --taskbar-text) into window or content-area components
Don't use --color-accent for static decorative elements — reserve it to signal interactivity

Typography

Two font stacks: Google Sans for display, system-ui for UI. Click any row to copy.


Display / Headings
Google Sans
400, 700 weight — imported via @fontsource/google-sans
UI / Body
System UI
--font-family — native system stack, zero download cost

Hierarchy Reference

TokenSizeUI RoleFontWeight
--font-size-3xl2remPage titles, hero headlinesGoogle Sans700
--font-size-2xl1.5remSection headingsGoogle Sans700
--font-size-xl1.25remSub-headings, window titlesSystem UI600
--font-size-lg1.125remLead paragraphs, calloutsSystem UI400
--font-size-base1remBody text, paragraphsSystem UI400
--font-size-sm0.875remCaptions, labels, metadataSystem UI400–500
Do
Use Google Sans only for display and heading text (--font-size-2xl and above)
Use --font-size-sm at weight 500 for uppercase labels, captions, and micro-copy
Step through adjacent sizes — each level in the scale is one step from the last
Don't
Don't use Google Sans for body text — readability degrades below 1.125rem
Don't skip sizes (e.g. jumping from base to 3xl) — intermediate steps maintain visual rhythm
Don't use --font-size-sm for paragraph text — minimum readable body size is --font-size-base

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.


TokenValueNotes
--motion-duration200msStandard transition speed. Fast enough to feel snappy, slow enough to register.
--motion-easingease-outDecelerating 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

Do
Use motion to confirm actions (button press, form submit), orient users (window opening from click origin), and communicate state changes
Always reference var(--motion-duration) and var(--motion-easing) in CSS transitions — never hardcode 200ms
Test with prefers-reduced-motion enabled — the CSS tokens automatically collapse to 0ms
Don't
Don't animate purely for decoration — every animation must serve a functional purpose
Don't use motion for content that was already visible — animate only transitions between states
Don't add custom animations outside the token system without also adding a prefers-reduced-motion override

Elevation

Shadow tokens map to the OS window hierarchy — resting, focused, dropdown, and taskbar.


--window-shadow
Resting window elevation — subtle depth for primary windows
--window-shadow-focused
Active / focused window — stronger shadow signals user attention
--dropdown-shadow
Dropdown menus and popovers
--taskbar-shadow
Taskbar separator — barely-there lift

Z-Index

Three explicit layers model the OS stacking context.


1000
--z-screensaver
Screensaver and boot intro overlay
10
--z-secondary
Floating secondary windows (draggable)
1
--z-primary
Desktop content, primary window layer

Button

Styled HTML button with primary and secondary variants.


A styled HTML button with two visual variants. Uses CSS custom properties for all colors, transitions, and spacing — making it automatically theme-aware without any JavaScript.

src/components/ui/Button.tsx

Variants

Live demo

Props

PropTypeDefaultDescription
variant'primary' | 'secondary''primary'Visual style of the button
childrenReact.ReactNodeButton label or content
...restButtonHTMLAttributesAll standard HTML button attributes (type, onClick, disabled, etc.)

Accessibility

  • Always set type="button" or type="submit" to prevent accidental form submission
  • Focus ring uses --color-focus and --color-focus-ring — visible at WCAG AA contrast
  • Disabled state must use the disabled attribute, not just opacity — this removes the element from the focus order and announces it to screen readers
  • Primary vs secondary is a visual distinction only — both are announced as "button" by screen readers

Do's & Don'ts

Do
Use Primary for the single most important action in a view
Use Secondary for alternatives, cancellation, or lower-priority actions
Set type="button" on all non-submit buttons
Don't
Don't use Button for navigation — use an <a> element instead
Don't place two Primary buttons side by side in the same context
Don't rely on opacity alone for disabled — use the disabled attribute

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.tsx

Variants

Position variants — hover each button

Props

PropTypeDefaultDescription
contentstringText to display in the tooltip (required)
position'top' | 'bottom' | 'left' | 'right''top'Preferred position. Overridden by autoPosition when near viewport edges
delaynumber200Milliseconds before the tooltip appears on hover
disabledbooleanfalsePrevents the tooltip from showing
keyboardShortcutstringAppended to tooltip text in parentheses — e.g. ⌘K
autoPositionbooleantrueRecalculates position to avoid viewport clipping
multilinebooleanfalseAllows tooltip text to wrap for longer content

Accessibility

  • Tooltip element has role="tooltip" and aria-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-motion is set

Do's & Don'ts

Do
Use for supplemental information that isn't critical to complete the task
Show keyboard shortcuts with the keyboardShortcut prop
Keep tooltip text short — one sentence maximum
Don't
Don't put interactive content (links, buttons) inside tooltips
Don't use tooltips for required information — it must always be visible
Don't disable autoPosition unless you have a controlled scroll container

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.tsx

Variants

Live demo

Props

PropTypeDefaultDescription
isOpenbooleanControls whether the lightbox is visible (required)
imageSrcstring | nullURL of the image to display (required)
altstringAccessible alt text for the image (required)
onClose() => voidCalled when user closes the lightbox (required)
onNext() => voidCalled when user navigates to next image. Renders next button when provided
onPrevious() => voidCalled when user navigates to previous image. Renders prev button when provided
currentIndexnumberZero-based index of the current image for the counter display
totalImagesnumberTotal number of images — counter shows when this is > 1

Accessibility

  • Escape key closes the lightbox; ArrowLeft/ArrowRight navigate between images
  • Locks body.style.overflow to hidden while open — prevents background scroll
  • All controls (close, previous, next) have explicit aria-label attributes
  • Rendered via createPortal on document.body — avoids z-index stacking context issues
  • Clicking the overlay backdrop closes the lightbox

Do's & Don'ts

Do
Always provide meaningful alt text — describe the image content, not its filename
Include onNext, onPrevious, currentIndex, and totalImages for galleries with multiple images
Manage open/closed state in the parent component with useState
Don't
Don't use Lightbox for video or interactive content
Don't omit the onClose handler — users must be able to dismiss the overlay
Don't use imageSrc="" — always guard against null/empty before opening

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.tsx

Variants

Live demo — click tabs
High-level summary of the project goals and outcomes.

Props

PropTypeDefaultDescription
activeTab'Overview' | 'Decisions' | 'Principles' | 'Changes' | 'Issues'The currently active tab (required, controlled)
onTabChange(tab: TabType) => voidCallback fired when the user selects a different tab (required)

Accessibility

  • Tab container has role="tablist"; each button has role="tab"
  • aria-selected="true" is set on the active tab; aria-label on each tab button
  • Active indicator is a 2px border-bottom line — never communicated by color alone
  • Each tab has an aria-label appending "tab" (e.g. "Overview tab")

Do's & Don'ts

Do
Use tabs for switching between views of the same object — not for page navigation
Keep tab labels short (1–2 words)
Manage state in the parent and pass activeTab as a controlled prop
Don't
Don't use tabs to navigate between separate pages — use nav links
Don't exceed 5–6 tabs — use a dropdown or secondary nav for more
Don't change the URL when switching tabs unless the view is independently linkable

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.tsx

Variants

Resting state
Window Title
Window content lives here. Every secondary window in the portfolio uses this chrome — consistent border-radius, shadow tokens, and traffic-light controls in red / yellow / green order.
Focused state (stronger shadow)
Active Window
The focused window uses --window-shadow-focused — a stronger shadow that visually brings it forward and signals user attention.

Key Tokens

PropTypeDefaultDescription
--window-shadowshadow token0 4px 12px rgba(0,0,0,0.1)Resting window elevation
--window-shadow-focusedshadow token0 8px 24px rgba(0,0,0,0.15)Active / focused window — stronger shadow
--window-border-radiusCSS var8pxConsistent corner radius for all windows

Accessibility

  • Traffic light buttons should have aria-label values: "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

Do
Always use 8px border-radius (--window-border-radius) on all windows
Traffic lights must always appear in red / yellow / green order
Use --window-shadow-focused for the topmost / most recently interacted window
Don't
Don't nest secondary windows inside each other
Don't override the shadow tokens per-window — use the shared tokens
Don't change the traffic light colors — they are a universal macOS metaphor

Tags & Pills

Compact pill labels in default, accent, and success variants.


Compact pill-shaped labels used throughout case studies, project cards, and metadata displays. Three semantic variants map to the color token system: default (neutral), accent (brand), and success (positive).

Pattern — case studies, project metadata

Variants

All variants
DefaultAccentSuccess
In context — skill tags
Product DesignUX ResearchDesign SystemsFeaturedShipped

Styles Reference

PropTypeDefaultDescription
defaultclass variantNeutral — background: --color-surface, border: --color-border
accentclass variantBrand indigo — 10% accent background with accent text and border
successclass variantPositive green — 10% success background with success text and border

Accessibility

  • Tags are purely presentational <span> elements by default
  • When a list of tags conveys structured meaning, wrap in a <ul> with aria-label
  • Variant color is never the only signal — the text content always carries meaning
  • If a tag is clickable (filter action), add role="button" and keyboard support

Do's & Don'ts

Do
Keep tag text short — 1 to 3 words maximum
Use Success variant only for genuinely positive states (shipped, live, approved)
Use Accent to highlight the most important or featured tag in a set
Don't
Don't use tags as interactive buttons without adding role="button" and keyboard events
Don't use Accent and Success in the same tag set for unrelated categories
Don't put long sentences in tags — truncate or use a different element

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 cards

Variants

Hover to see elevation

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

PropTypeDefaultDescription
box-shadowCSS propertynoneResting state — uses var(--color-border) for the outline border
box-shadow (hover)CSS propertyvar(--window-shadow)Elevated state — uses window shadow token on hover
transform (hover)CSS propertytranslateY(-2px)Subtle lift — 2px upward translation on hover
transitionCSS property200ms ease-outUses --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

Do
Use consistent padding — --spacing-md (16px) for compact, --spacing-lg (24px) for standard cards
Wrap the whole card in <a> if clicking anywhere on it navigates
Use the window shadow token (--window-shadow) for the hover state, not a custom value
Don't
Don't nest clickable elements inside an already-clickable card
Don't mix card sizes in the same grid without intentional hierarchy
Don't use custom shadow values — always reference --window-shadow or --window-shadow-focused

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.tsx

Variants

Focus each input to see the focus ring

Focus Ring Tokens

PropTypeDefaultDescription
border-colorCSS propertyvar(--color-border)Default border — changes to --color-focus on :focus
box-shadowCSS propertynoneOn :focus: 0 0 0 3px var(--color-focus-ring) — visible ring glow
--color-focustoken#6366f1 / #818cf8Focus border color — matches accent in light and dark
--color-focus-ringtokenrgba(99,102,241,0.3)Semi-transparent glow around focused inputs

Accessibility

  • Every input must have an associated <label> via htmlFor / id — placeholder is not a substitute
  • Required fields should use both required attribute and aria-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

Do
Always show a visible label — above the input, never as a floating label that overlaps
Group related fields (name + email) in a <fieldset> with a <legend>
Use type="email", type="tel" etc. — enables native mobile keyboards and browser autocomplete
Don't
Don't use placeholder text as the only label — it disappears when the user types
Don't disable browser autocomplete (autocomplete="off") without a strong reason
Don't remove the focus ring — this is a critical accessibility requirement

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.tsx

Variants

Three states: empty · with content · active drag handle
Scratch Pad
Start typing…

Empty

Scratch Pad
Consider motion curve for window open. Check contrast on taskbar icons.

With content

Dragging…
Drag from header only ↑

Dragging

Behaviour

PropTypeDefaultDescription
content persistencelocalStoragesticky-note-contentText survives full page reloads. Content versioning key prevents stale data from old schema.
position persistencelocalStoragesticky-note-positionStores { x, y } pair. Resets to default corner position if no stored value found.
drag targetheader onlyDragging is only activated on the note header — the content area allows text selection freely.
default position (desktop)CSSright: --spacing-md, top: 52pxAppears in top-right, just below the taskbar.
default position (mobile)CSSright: --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", and aria-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
Do
Use this as a single persistent scratchpad — one instance per desktop
Drag from the header bar only; the content area is reserved for text selection
Clear localStorage key sticky-note-content to reset if content feels stale
Don't
Don't render multiple StickyNote instances — there is only one localStorage slot
Don't add click handlers to the content area — it will break text selection
Don't rely on StickyNote for persistent data — it is display-only and can be cleared

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.tsx

Variants

AQI level states: Loading skeleton · Good · Moderate · Unhealthy · Very Unhealthy

Loading

32
Good
PM2.58 µg
Ozone14 ppb

Good (0–50)

78
Moderate
PM2.522 µg
Ozone38 ppb

Moderate (51–100)

124
Unhealthy
PM2.545 µg
PM1062 µg

Unhealthy (101–150)

185
Very Unhealthy
PM2.588 µg
PM10112 µg

Very Unhealthy (151–200)

Props

PropTypeDefaultDescription
locationDataLocationDatarequiredIP-based location fallback: { ip, state, country, latitude, longitude }
gpsCoords{ lat: number; lng: number } | nullnullPrecise GPS coordinates. When available, preferred over IP-based coords for the API call.

AQI Scale

PropTypeDefaultDescription
0–50GoodGreenAir quality is satisfactory; little to no risk.
51–100ModerateYellowAcceptable; unusually sensitive people may be affected.
101–150Unhealthy (sensitive)OrangeSensitive groups may experience effects.
151–200UnhealthyRedEveryone may begin to experience health effects.
201–300Very UnhealthyPurpleHealth 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" and aria-label="Air Quality Index"
  • Portal renders into #desktop-cards-root (falls back to document.body)
  • Position persisted via localStorage key aqi-card-position
Do
Prefer GPS coords over IP coords — pass gpsCoords when the Geolocation API grants permission
Let the card auto-refetch; do not manually clear or reset the interval
Use the AQI color scale consistently — green/yellow/orange/red/purple map directly to health risk levels
Don't
Don't hardcode coordinates — always pass locationData and let the card decide which to use
Don't render this inside a window — it is a desktop-layer widget rendered via portal
Don't use the AQI color for anything other than AQI — it is a semantic health indicator

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.tsx

Variants

5 of 9 weather themes — background, text, and icon palette all shift per theme
☀️
24°
Clear sky
H:27° L:18°12 km/h

clearDay

🌙
17°
Clear night
H:24° L:14°8 km/h

clearNight

🌧️
14°
Light rain
H:16° L:10°22 km/h

rainy

⛈️
11°
Thunderstorm
H:13° L:8°34 km/h

stormy

❄️
-2°
Light snow
H:0° L:-5°10 km/h

snowy

Props

PropTypeDefaultDescription
locationDataLocationDatarequiredIP-based location fallback: { ip, state, country, latitude, longitude }
gpsCoords{ lat: number; lng: number } | nullnullGPS coordinates preferred over IP coords when available.

Weather Themes

PropTypeDefaultDescription
clearDaydata-weather-themeBright blue gradient; sunny day palette
clearNightdata-weather-themeDeep navy gradient; night sky palette
cloudyDaydata-weather-themeMuted grey-blue; overcast day palette
cloudyNightdata-weather-themeDark slate; overcast night palette
rainydata-weather-themeCool blue-grey; rain palette
snowydata-weather-themePale blue-white; snow palette
stormydata-weather-themeDark purple-grey; storm palette
foggydata-weather-themeMuted warm grey; fog palette
defaultdata-weather-themefallbackNeutral 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" and aria-label="Weather"
  • Portal renders into #desktop-cards-root (falls back to document.body)
  • Position persisted via localStorage key weather-card-position
Do
Let the weather theme apply automatically via data-weather-theme — do not override card backgrounds manually
Prefer GPS coords when available to get the most accurate local conditions
Use the default theme as the safe fallback when API returns an unknown condition
Don't
Don't render this inside a window or card — it is a portal-based desktop widget
Don't add extra API calls in child components — the card manages its own fetch lifecycle
Don't hardcode coordinates — pass locationData so the card resolves the best source

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.tsx

Variants

6 iconType values — click to see selected state
💼
Work
🕹️
Playground
✏️
Thoughts
🙋
About
✉️
Contact
🗑️
Trash

Props

PropTypeDefaultDescription
idstringrequiredUnique identifier for this icon instance
iconType'work' | 'playground' | 'thoughts' | 'about' | 'contact' | 'resume' | 'trash' | 'folder' | 'image' | 'file'requiredDetermines which icon SVG to render
labelstringrequiredVisible text label below the icon
windowTypestring (12 values)requiredWindow type to open on click — passed to openWindowViaEvent()
windowTitlestringrequiredTitle shown in the opened window chrome
windowContentReact.ReactNoderequiredContent rendered inside the opened window
initialXnumberrequiredStarting X position relative to the desktop container
initialYnumberrequiredStarting Y position (container offset = 42px for taskbar)
onPositionChange(id, x, y) => voidrequiredCallback fired after drag ends with updated coordinates
isSelectedbooleanfalseApplies selected visual state (highlight ring)
onSelect() => voidundefinedFires when icon receives focus or is clicked without dragging
onDeselect() => voidundefinedFires 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.pushState updates the URL to the window's route
  • Origin animation — captures the icon's bounding rect and passes it to openWindowViaEvent for 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
Do
Always provide onPositionChange to persist icon position — otherwise drags are lost on reload
Use iconType to match the semantic type of what the icon opens (e.g. work → work window)
Keep label text short (1–2 words) — it renders below the icon in a small, truncated caption
Don't
Don't place icons at y < 42px — the taskbar occupies that zone and will overlap
Don't skip the drag threshold logic — re-implementing it outside this component will cause mis-fires
Don't hardcode window content as a string — windowContent expects React.ReactNode

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.tsx

Variants

Full taskbar anatomy — logo · nav buttons · right slot · desktop below
Biswarup MondalSr. Product Designer
WorkPlaygroundThoughtsDesign SystemContactAbout
🔋 84%Thu Mar 5 3:42 PM
Desktop canvas (icons, windows, widgets)

Structure

PropTypeDefaultDescription
Logo anchorAstro (static)href="/"SVG monogram + name/title text. Two SVGs for light/dark (CSS display toggled by data-theme).
TaskbarButtonsReact island (client:load)Renders nav buttons: Work, Playground, Thoughts, Design System, Contact, About. Each opens a window or external tab.
MobileNavDropdownReact island (client:load)Chevron dropdown replacing nav buttons at ≤768px.
taskbar-right slotDOM slot#taskbar-right-slotEmpty slot for right-side components injected by other React islands (e.g. SystemStatus, ThemeSwitcher).

Design Tokens

PropTypeDefaultDescription
--taskbar-textCSS vartheme-awareNav button and logo name text color
--taskbar-text-secondaryCSS vartheme-awareLogo subtitle (role/title) text color
--taskbar-shadowCSS varsubtle drop shadowBottom edge shadow — separates bar from desktop content
heighthardcoded42pxFixed taskbar height — desktop icon positioning uses this value as top offset
backdrop-filterCSSblur(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-buttonsdisplay: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-only class.

Accessibility

  • <nav role="toolbar" aria-label="Main navigation"> wraps the entire bar
  • Logo link has aria-label="Home" and SVGs are aria-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 to none on logo and buttons
Do
Add new nav items in TaskbarButtons.tsx only — do not modify Taskbar.astro for nav changes
Use --taskbar-text and --taskbar-text-secondary for any text injected into the taskbar-right slot
Keep the taskbar height at 42px — desktop icon layout depends on this fixed value
Don't
Don't add background-color to .taskbar-tab-button — the bar uses translucent chrome that must show through
Don't use accent color for taskbar text on hover — it breaks the OS-chrome aesthetic
Don't render content-area components inside the taskbar — it is navigation chrome only

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.tsx

Variants

5 battery states · clock always shows alongside battery
62%+Thu Mar 5 3:42 PM
Charging
84%Thu Mar 5 3:42 PM
High ≥50%
35%Thu Mar 5 3:42 PM
Low 20–49%
12%Thu Mar 5 3:42 PM
Critical <20%
ACThu Mar 5 3:42 PM
AC / Desktop

Props

No props — fully self-contained. All data is derived from the browser's Date API and the navigator.getBattery() API.

Battery States

PropTypeDefaultDescription
Chargingstate--color-accentBattery icon + level shown in accent color; "+" charging indicator appended
≥50%state--color-textNormal battery level — default text color
20–49%state#f59e0b (amber)Low battery warning color
<20%state#dc2626 (red)Critical battery level color
API unsupportedstate--color-text-secondaryShows "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 title tooltip on hover
  • Clock interval: 1 second via setInterval — cleared on unmount

Accessibility

  • Battery container has a title attribute with full battery status text
  • Battery SVG icon is aria-hidden="true" — the text label carries the meaning
  • Hidden on mobile via .desktop-only class (not rendered in the DOM on small screens)
Do
Inject SystemStatus into the #taskbar-right-slot — it is designed to live in taskbar chrome
Let the component manage its own timer and battery listeners — do not lift interval state up
Use the "AC" fallback gracefully — most desktop browsers do not expose the Battery API
Don't
Don't render SystemStatus inside windows or cards — it is OS-chrome, not content
Don't hardcode a time string — always use the live Date state for accurate display
Don't add interactivity to the battery or clock — they are read-only status indicators

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)