Instantly share code, notes, and snippets.
Forked from sorenblank/sidebar-vercel-design-eng.tsx
Created
October 11, 2025 10:12
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save image72/447ffebc7e9cc6644d3dfa7a004ecdc9 to your computer and use it in GitHub Desktop.
Here is the preview: https://sorenblank.com/snippets/sidebar-vercel // And this was originally published on Twitter: https://twitter.com/sorenblank/status/1976394237899047313
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| // Import necessary utilities and components. | |
| // tailwind-merge is used to intelligently merge Tailwind CSS classes, preventing conflicts. | |
| import { twMerge } from "tailwind-merge"; | |
| // useState is a React Hook for managing state within the component. | |
| import { useState } from "react"; | |
| // Custom icon components for the sidebar. | |
| import { | |
| SidebarIcon, | |
| SearchIcon, | |
| ClockIcon, | |
| StarIcon, | |
| TrashIcon, | |
| SettingsIcon, | |
| PlusIcon, | |
| } from "@/assets/sidebarVercelIcons"; | |
| /** | |
| * VercelSidebar Component | |
| * | |
| * A responsive sidebar layout inspired by Vercel's UI. | |
| * It features a collapsible sidebar that can be toggled and also reveals itself on hover. | |
| * The hover-reveal functionality is triggered by a specific icon button, | |
| * achieved using modern CSS features with Tailwind's arbitrary variants. | |
| */ | |
| const VercelSidebar = () => { | |
| // State to manage the open/closed state of the sidebar. | |
| // `isOn` is true when the sidebar is open (slid out of view) and false when closed (visible). | |
| const [isOn, setIsOn] = useState(false); | |
| // Toggles the sidebar state between open and closed. | |
| const handleToggle = () => { | |
| setIsOn(!isOn); | |
| }; | |
| return ( | |
| // Main container for the entire page layout. | |
| <main className="bg-background relative flex h-screen max-h-screen flex-col overflow-y-hidden p-2 transition-all duration-200 ease-in-out"> | |
| {/* Mobile Message */} | |
| <div className="flex h-full w-full flex-col items-center justify-center p-8 text-center md:hidden"> | |
| <p className="text-h2 text-subdued">Go to desktop.</p> | |
| <p className="text-body-md text-subdued mt-4"> | |
| I didn't have time for mobile responsiveness, I have a life. | |
| </p> | |
| </div> | |
| {/* Desktop Content */} | |
| <div className="hidden h-full w-full flex-col md:flex"> | |
| {/* Header Section */} | |
| <div className="flex flex-row items-center gap-4 p-4"> | |
| {/* Brand Logo */} | |
| <img src="/brand.png" className="h-6 w-6" /> | |
| {/* Decorative Separator */} | |
| <div className="bg-primary h-6 w-[1px] -skew-12" /> | |
| {/* Brand Name/Title */} | |
| <p className="text-h3 text-subdued">Soren</p> | |
| </div> | |
| {/* Main Content Wrapper */} | |
| {/* | |
| This div is the parent container for the sidebar and the main content. | |
| The `group` class is crucial here. It allows child elements to change their style | |
| based on the state of this parent container (e.g., when it's hovered). | |
| We use this with the `:has()` pseudo-class to create a "reverse" group hover effect. | |
| */} | |
| <div className="group flex min-h-0 w-full flex-1 flex-row gap-4 p-2 transition-all duration-200 ease-in-out"> | |
| {/* --- Sidebar Section --- */} | |
| {/* | |
| This is the fixed part of the sidebar that slides in and out. | |
| `twMerge` is used to conditionally apply classes. | |
| When `isOn` is true, '-ml-[272px]' is applied to slide it off-screen to the left. | |
| */} | |
| <div | |
| className={twMerge( | |
| "bg-background relative h-full min-w-3xs rounded-xl p-2 transition-all duration-200 ease-in-out", | |
| isOn && "-ml-[272px]", | |
| )} | |
| > | |
| {/* --- Hover-Reveal Sidebar Content --- */} | |
| {/* | |
| This inner div is the floating panel that appears when the sidebar is "open" (`isOn`). | |
| It becomes visible and slides into view when the trigger element is hovered. | |
| - `isOn` state: Applies base styles for the floating panel (size, position, etc.). | |
| - `group-has-[.sidebar-icon-trigger:hover]:ml-[240px]`: This is the key. | |
| If the parent `.group` has a descendant `.sidebar-icon-trigger` that is being hovered, | |
| this panel will slide into view. | |
| - `group-has-[.sidebar-wrapper:hover]:ml-[240px]`: This allows the panel to *stay* open | |
| if the user moves their cursor onto the panel itself after triggering it. | |
| - `!isOn`: Hides the panel when the sidebar is in its default closed state. | |
| */} | |
| <div | |
| className={twMerge( | |
| "sidebar-wrapper border-secondary/50 absolute left-0 h-full w-full rounded-lg transition-all duration-200 ease-in-out", | |
| isOn && | |
| "bg-background rounded-lg-primary z-10 h-11/12 w-3xs translate-y-12 border p-2 pl-6 group-has-[.sidebar-icon-trigger:hover]:ml-[240px] group-has-[.sidebar-wrapper:hover]:ml-[240px]", | |
| !isOn && "ml-[0px] h-full border-transparent bg-transparent", | |
| )} | |
| > | |
| {/* Content inside the hoverable panel */} | |
| <div className="flex h-full flex-col gap-2"> | |
| {/* New Chat Button */} | |
| <button className="bg-button-bg hover:bg-button-hover text-body-sm flex w-full items-center justify-center gap-2 rounded-lg py-2 transition-colors duration-0"> | |
| <PlusIcon className="text-subdued" size={16} /> | |
| New Chat | |
| </button> | |
| {/* Search Bar */} | |
| <div className="bg-input-bg relative w-full rounded-lg"> | |
| <div className="pointer-events-none absolute inset-y-0 left-3 flex items-center"> | |
| <SearchIcon className="text-subdued" size={14} /> | |
| </div> | |
| <input | |
| type="text" | |
| placeholder="Search..." | |
| className="text-body-sm placeholder:text-subdued w-full rounded-lg bg-transparent py-2 pr-3 pl-9 focus:outline-none" | |
| /> | |
| </div> | |
| {/* Navigation Menu */} | |
| <nav className="flex flex-1 flex-col gap-1 overflow-y-auto"> | |
| {/* Recent Section */} | |
| <div className="mt-2 mb-2"> | |
| <button className="hover:bg-button-hover text-subdued text-body-xs flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors duration-0"> | |
| <ClockIcon size={14} /> | |
| <span>Recent</span> | |
| </button> | |
| </div> | |
| {/* Chat History Items */} | |
| <div className="space-y-1"> | |
| {[ | |
| "Design system components", | |
| "API integration help", | |
| "React best practices", | |
| "TypeScript generics", | |
| "CSS animations", | |
| "Next.js routing", | |
| "Database optimization", | |
| "Authentication flow", | |
| ].map((chat, index) => ( | |
| <button | |
| key={index} | |
| id={`recent-${index}`} | |
| className="hover:bg-button-hover text-body-xs group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left transition-colors duration-0" | |
| > | |
| <span className="text-subdued truncate">{chat}</span> | |
| </button> | |
| ))} | |
| </div> | |
| {/* Starred Section */} | |
| <div className="mt-4 mb-2"> | |
| <button className="hover:bg-button-hover text-subdued text-body-xs flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors duration-0"> | |
| <StarIcon size={14} /> | |
| <span>Starred</span> | |
| </button> | |
| </div> | |
| {/* Starred Items */} | |
| <div className="space-y-1"> | |
| {[ | |
| "Important project notes", | |
| "Code snippets collection", | |
| ].map((starred, index) => ( | |
| <button | |
| key={index} | |
| className="hover:bg-button-hover text-body-xs group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left transition-colors duration-0" | |
| > | |
| <span className="text-subdued truncate">{starred}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </nav> | |
| {/* Bottom Actions */} | |
| <div className="border-secondary/50 space-y-1 border-t pt-2"> | |
| <button className="hover:bg-button-hover text-subdued text-body-xs flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors duration-0"> | |
| <TrashIcon size={14} /> | |
| <span>Trash</span> | |
| </button> | |
| <button className="hover:bg-button-hover text-subdued text-body-xs flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors duration-0"> | |
| <SettingsIcon size={14} /> | |
| <span>Settings</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* --- Main Content Area --- */} | |
| {/* This is the main panel on the right side. */} | |
| <div className="bg-foreground border-secondary/50 h-full w-full rounded-xl border-[1px] transition-all duration-200 ease-in-out"> | |
| {/* | |
| This div has the class `sidebar-icon-trigger`. It serves as the hover target. | |
| When the user's cursor enters this div, the `group-has` condition is met, | |
| which triggers the animation on the sidebar panel. | |
| */} | |
| <div className="sidebar-icon-trigger max-w-max p-2"> | |
| {/* The actual button that toggles the sidebar's open/closed state on click. */} | |
| <button | |
| className="hover:bg-button-hover rounded-lg p-2" | |
| onClick={handleToggle} | |
| > | |
| <SidebarIcon className="text-subdued" /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| ); | |
| }; | |
| export default VercelSidebar; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment