Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions FOUC_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Theme Toggle FOUC Fix

## Problem
The dark mode/light mode theme toggle button appeared large and unstyled during initial page load on slow network connections, causing a "Flash of Unstyled Content" (FOUC).

## Root Cause
The theme toggle button was rendering before the main CSS stylesheet (`global.css`) loaded. This caused the SVG icons to display at their default size without proper styling, creating a jarring visual experience.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls add the description in PR itself and rm this file.


## Solution Implemented

### 1. Critical CSS Inline Styles (BaseLayout.astro)
Added critical CSS directly in the `<head>` section to ensure the theme toggle is styled immediately, even before Tailwind CSS loads:

```html
<style>
/* Prevent theme toggle FOUC by inlining critical styles */
#theme-switcher-container {
display: grid;
grid-template-columns: 1fr;
}
/* Container styling with responsive padding */
#theme-switcher-container > div {
position: relative;
z-index: 0;
display: inline-grid;
gap: 0.125rem;
border-radius: 9999px;
background-color: rgba(0, 0, 0, 0.05);
padding: 0.125rem;
color: rgb(9, 9, 11);
}
/* Dark mode styles */
.dark #theme-switcher-container > div {
background-color: rgba(255, 255, 255, 0.1);
color: rgb(255, 255, 255);
}
/* Button/icon container sizing */
#theme-switcher-container button,
#theme-switcher-container > div > div {
position: relative;
border-radius: 9999px;
cursor: pointer;
transition: all 0.2s;
padding: 0.125rem;
}
/* SVG icon sizing - responsive */
#theme-switcher-container svg {
width: 1rem;
height: 1rem;
}
/* Tablet sizing */
@media (min-width: 640px) {
#theme-switcher-container > div {
padding: 0.1875rem;
}
#theme-switcher-container button,
#theme-switcher-container > div > div {
padding: 0.25rem;
}
#theme-switcher-container svg {
width: 1.25rem;
height: 1.25rem;
}
}
/* Desktop sizing */
@media (min-width: 1024px) {
#theme-switcher-container svg {
width: 1.5rem;
height: 1.5rem;
}
}
/* Active theme button styling */
#theme-switcher-container .theme-active {
background-color: rgb(255, 255, 255);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.dark #theme-switcher-container .theme-active {
background-color: rgb(75, 85, 99);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
}
</style>
```

### 2. Improved ThemeSwitcher Component (ThemeSwitcher.tsx)

#### Added ID for targeting
- Added `id="theme-switcher-container"` to both the skeleton and mounted states
- Added `theme-active` class to the active button state

#### Enhanced Skeleton State
The skeleton now displays both light and dark mode buttons with proper sizing, preventing layout shift:

```tsx
if (!mounted) {
return (
<div id="theme-switcher-container" className="grid grid-cols-1">
<div className="relative z-0 inline-grid grid-cols-2 gap-0.5 rounded-full bg-gray-950/5 p-0.5 sm:p-0.75 text-gray-950 dark:bg-white/10 dark:text-white">
<div className="relative rounded-full p-0.5 sm:p-1 lg:p-1 theme-active">
{/* Light mode icon with proper sizing */}
</div>
<div className="relative rounded-full p-0.5 sm:p-1 lg:p-1">
{/* Dark mode icon with proper sizing */}
</div>
</div>
</div>
);
}
```

## Benefits

1. **Instant Styling**: The theme toggle is styled immediately, even on slow connections
2. **No Layout Shift**: The skeleton state matches the final state exactly, preventing Cumulative Layout Shift (CLS)
3. **Consistent Experience**: Users see a properly sized button from the first paint
4. **Performance**: Critical CSS is minimal (~1KB) and doesn't block page rendering
5. **Responsive**: Works correctly across all device sizes (mobile, tablet, desktop)

## Testing

To verify the fix:

1. **Throttle Network**: Open Chrome DevTools > Network tab > Throttle to "Slow 3G"
2. **Hard Refresh**: Press Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)
3. **Observe**: The theme toggle should appear at its correct size immediately, without any flash or size change

## Files Modified

- `/Users/tirth/FreeDevTools/frontend/src/layouts/BaseLayout.astro` - Added critical inline CSS
- `/Users/tirth/FreeDevTools/frontend/src/components/theme/ThemeSwitcher.tsx` - Enhanced skeleton state and added IDs

## Technical Notes

- The critical CSS uses exact pixel values matching Tailwind's sizing system
- Responsive breakpoints match Tailwind's default breakpoints (sm: 640px, lg: 1024px)
- Dark mode styles are duplicated in critical CSS to ensure immediate application
- The `theme-active` class allows the inline CSS to target the active button state
29 changes: 23 additions & 6 deletions frontend/src/components/theme/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,27 @@ const ThemeSwitcher: React.FC = () => {
// Don't render until mounted to prevent SSR issues
if (!mounted) {
return (
<div className="grid grid-cols-1">
<div className="relative z-0 inline-grid grid-cols-2 md:grid-cols-3 gap-0.5 rounded-full bg-gray-950/5 p-0.75 text-gray-950 dark:bg-white/10 dark:text-white">
<div className="relative rounded-full p-1.5 *:size-7 bg-white ring ring-gray-950/10 sm:p-0">
{themeConfigs[1].icon}
<div id="theme-switcher-container" className="grid grid-cols-1">
<div className="relative z-0 inline-grid grid-cols-2 gap-0.5 rounded-full bg-gray-950/5 p-0.5 sm:p-0.75 text-gray-950 dark:bg-white/10 dark:text-white">
<div className="relative rounded-full p-0.5 sm:p-1 lg:p-1 theme-active">
<svg viewBox="0 0 28 28" fill="none" className="size-4 sm:size-5 lg:size-6">
Copy link
Contributor

@lovestaco lovestaco Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is hardcoding the svg in file required? We have many lucide icons,
if its absolutely required, pls put it in public dir and use its url
<img src="/freedevtools/svg_icons/comet/moon.svg">

Ex: https://hexmos.com/freedevtools/svg_icons/comet/moon.svg

<circle cx="14" cy="14" r="3.5" stroke="currentColor"></circle>
<path d="M14 8.5V6.5" stroke="currentColor" strokeLinecap="round"></path>
<path d="M17.889 10.1115L19.3032 8.69727" stroke="currentColor" strokeLinecap="round"></path>
<path d="M19.5 14L21.5 14" stroke="currentColor" strokeLinecap="round"></path>
<path d="M17.889 17.8885L19.3032 19.3027" stroke="currentColor" strokeLinecap="round"></path>
<path d="M14 21.5V19.5" stroke="currentColor" strokeLinecap="round"></path>
<path d="M8.69663 19.3029L10.1108 17.8887" stroke="currentColor" strokeLinecap="round"></path>
<path d="M6.5 14L8.5 14" stroke="currentColor" strokeLinecap="round"></path>
<path d="M8.69663 8.69711L10.1108 10.1113" stroke="currentColor" strokeLinecap="round"></path>
</svg>
</div>
<div className="relative rounded-full p-0.5 sm:p-1 lg:p-1">
<svg viewBox="0 0 28 28" fill="none" className="size-4 sm:size-5 lg:size-6">
<path d="M10.5 9.99914C10.5 14.1413 13.8579 17.4991 18 17.4991C19.0332 17.4991 20.0176 17.2902 20.9132 16.9123C19.7761 19.6075 17.109 21.4991 14 21.4991C9.85786 21.4991 6.5 18.1413 6.5 13.9991C6.5 10.8902 8.39167 8.22304 11.0868 7.08594C10.7089 7.98159 10.5 8.96597 10.5 9.99914Z" stroke="currentColor" strokeLinejoin="round"></path>
<path d="M16.3561 6.50754L16.5 5.5L16.6439 6.50754C16.7068 6.94752 17.0525 7.29321 17.4925 7.35607L18.5 7.5L17.4925 7.64393C17.0525 7.70679 16.7068 8.05248 16.6439 8.49246L16.5 9.5L16.3561 8.49246C16.2932 8.05248 15.9475 7.70679 15.5075 7.64393L14.5 7.5L15.5075 7.35607C15.9475 7.29321 16.2932 6.94752 16.3561 6.50754Z" fill="currentColor" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"></path>
<path d="M20.3561 11.5075L20.5 10.5L20.6439 11.5075C20.7068 11.9475 21.0525 12.2932 21.4925 12.3561L22.5 12.5L21.4925 12.6439C21.0525 12.7068 20.7068 13.0525 20.6439 13.4925L20.5 14.5L20.3561 13.4925C20.2932 13.0525 19.9475 12.7068 19.5075 12.6439L18.5 12.5L19.5075 12.3561C19.9475 12.2932 20.2932 11.9475 20.3561 11.5075Z" fill="currentColor" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
</div>
</div>
</div>
Expand All @@ -165,7 +182,7 @@ const ThemeSwitcher: React.FC = () => {
// );

return (
<div className="grid grid-cols-1">
<div id="theme-switcher-container" className="grid grid-cols-1">
<div
className="relative z-0 inline-grid gap-0.5 rounded-full bg-gray-950/5 p-0.5 sm:p-0.75 text-gray-950 dark:bg-white/10 dark:text-white"
style={{
Expand All @@ -180,7 +197,7 @@ const ThemeSwitcher: React.FC = () => {
"p-0.5 *:size-4 sm:p-1 sm:*:size-5 lg:p-1 lg:*:size-6"
} ${
theme === config.type
? "bg-white ring ring-gray-950/10 dark:bg-gray-600 dark:ring-white/10"
? "theme-active bg-white ring ring-gray-950/10 dark:bg-gray-600 dark:ring-white/10"
: "hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => handleThemeChange(config.type)}
Expand Down
72 changes: 72 additions & 0 deletions frontend/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,78 @@ const GA_MEASUREMENT_ID = 'G-WXSDF484XZ';
<link rel="stylesheet" href="/freedevtools/fonts/fonts.css">


<!-- Critical CSS for Theme Switcher to prevent FOUC -->
<style>
/* Prevent theme toggle FOUC by inlining critical styles */
#theme-switcher-container {
display: grid;
grid-template-columns: 1fr;
}

#theme-switcher-container > div {
position: relative;
z-index: 0;
display: inline-grid;
gap: 0.125rem;
border-radius: 9999px;
background-color: rgba(0, 0, 0, 0.05);
padding: 0.125rem;
color: rgb(9, 9, 11);
}

.dark #theme-switcher-container > div {
background-color: rgba(255, 255, 255, 0.1);
color: rgb(255, 255, 255);
}

#theme-switcher-container button,
#theme-switcher-container > div > div {
position: relative;
border-radius: 9999px;
cursor: pointer;
transition: all 0.2s;
padding: 0.125rem;
}

#theme-switcher-container svg {
width: 1rem;
height: 1rem;
}

@media (min-width: 640px) {
#theme-switcher-container > div {
padding: 0.1875rem;
}

#theme-switcher-container button,
#theme-switcher-container > div > div {
padding: 0.25rem;
}

#theme-switcher-container svg {
width: 1.25rem;
height: 1.25rem;
}
}

@media (min-width: 1024px) {
#theme-switcher-container svg {
width: 1.5rem;
height: 1.5rem;
}
}

#theme-switcher-container .theme-active {
background-color: rgb(255, 255, 255);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}

.dark #theme-switcher-container .theme-active {
background-color: rgb(75, 85, 99);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
}
</style>

<!-- Theme initialization script -->
<script>
(function() {
Expand Down