Overview
Flow Browser’s tab management system displays tabs in vertical groups within the sidebar, supporting drag-and-drop reordering, audio indicators, and multi-space organization.
Tab Data Structure
TabData Interface
interface TabData {
id : number ;
title : string ;
url : string ;
faviconURL : string | null ;
loading : boolean ;
audible : boolean ;
muted : boolean ;
asleep : boolean ;
// ... additional properties
}
TabGroup Structure
interface TabGroup {
id : string ;
tabs : TabData [];
focusedTab : TabData | null ;
profileId : string ;
spaceId : string ;
}
Individual tab display within the sidebar.
Location : src/renderer/src/components/browser-ui/browser-sidebar/_components/tab-group.tsx:26
import { SidebarTab } from "@/components/browser-ui/browser-sidebar/_components/tab-group" ;
< SidebarTab tab = { tabData } isFocused = { true } />
Props
The tab data object containing title, URL, favicon, etc.
Whether this tab is currently focused/active
Features
{ ! noFavicon && (
< img
src = { craftActiveFaviconURL ( tab . id , tab . faviconURL ) }
alt = { tab . title }
className = { cn (
"size-full rounded-sm object-contain" ,
tab . asleep && "grayscale"
) }
onError = { () => setIsError ( true ) }
/>
)}
{ noFavicon && (
< div className = "size-full bg-gray-300 dark:bg-gray-300/30 rounded-sm" />
)}
Caches favicon URL to prevent flicker
Shows gray square fallback on error
Applies grayscale filter for sleeping tabs
< AnimatePresence initial = { false } >
{ ( isPlayingAudio || isMuted ) && (
< motion.button
initial = { { opacity: 0 , scale: 0.8 , width: 0 } }
animate = { { opacity: 1 , scale: 1 , width: "auto" } }
exit = { { opacity: 0 , scale: 0.8 , width: 0 } }
onClick = { handleToggleMute }
>
{ isMuted ? < VolumeX /> : < Volume2 /> }
</ motion.button >
) }
</ AnimatePresence >
Smooth spring animation for show/hide
Click to toggle mute state
Shows VolumeX when muted, Volume2 when playing
Interactions
Mouse Events :
const handleMouseDown = useCallback (( e : React . MouseEvent ) => {
if ( e . button === 0 ) {
// Left click - switch to tab
flow . tabs . switchToTab ( tab . id );
}
if ( e . button === 1 ) {
// Middle click - close tab
flow . tabs . closeTab ( tab . id );
}
}, [ tab . id ]);
const handleContextMenu = useCallback (( e : React . MouseEvent ) => {
e . preventDefault ();
flow . tabs . showContextMenu ( tab . id );
}, [ tab . id ]);
Memoization
SidebarTab uses React.memo with custom comparison:
const SidebarTab = memo (
function SidebarTab ({ tab , isFocused }) {
// Component implementation
},
( prev , next ) => {
// Only re-render if focus state or tab reference changes
return prev . isFocused === next . isFocused && prev . tab === next . tab ;
}
);
TabGroup Component
Container for a group of tabs with drag-and-drop support.
Location : src/renderer/src/components/browser-ui/browser-sidebar/_components/tab-group.tsx:190
import { TabGroup } from "@/components/browser-ui/browser-sidebar/_components/tab-group" ;
< TabGroup
tabGroup = { tabGroup }
isActive = { true }
isFocused = { true }
isSpaceLight = { true }
position = { 0 }
groupCount = { 5 }
moveTab = { moveTabHandler }
/>
Props
The tab group object containing tabs array and metadata
Whether this group contains the active tab
Whether this group is in the focused window/space
Whether the current space uses a light theme
Position index for drag-and-drop ordering
Total number of tab groups
moveTab
(tabId: number, newPosition: number) => void
required
Callback to move a tab to a new position
Drag and Drop System
Flow Browser uses @atlaskit/pragmatic-drag-and-drop for tab reordering.
Source Data
When dragging a tab group:
export type TabGroupSourceData = {
type : "tab-group" ;
tabGroupId : string ;
primaryTabId : number ;
profileId : string ;
spaceId : string ;
position : number ;
};
Draggable Setup
const draggableCleanup = draggable ({
element: el ,
getInitialData : () => ({
type: "tab-group" ,
tabGroupId: tabGroup . id ,
primaryTabId: tabs [ 0 ]?. id ,
profileId: tabGroup . profileId ,
spaceId: tabGroup . spaceId ,
position: position
})
});
Drop Target
const cleanupDropTarget = dropTargetForElements ({
element: el ,
getData : ({ input , element }) => {
return attachClosestEdge ({}, {
input ,
element ,
allowedEdges: [ "top" , "bottom" ]
});
},
canDrop : ( args ) => {
const sourceData = args . source . data as TabGroupSourceData ;
// Prevent dropping on self
if ( sourceData . tabGroupId === tabGroup . id ) {
return false ;
}
// Only allow same profile
if ( sourceData . profileId !== tabGroup . profileId ) {
return false ;
}
return true ;
},
onDrop : ( args ) => {
const edge = extractClosestEdge ( args . self . data );
const sourceData = args . source . data as TabGroupSourceData ;
let newPos : number | undefined ;
if ( edge === "top" ) {
newPos = position - 0.5 ;
} else if ( edge === "bottom" ) {
newPos = position + 0.5 ;
}
if ( sourceData . spaceId !== tabGroup . spaceId ) {
flow . tabs . moveTabToWindowSpace (
sourceData . primaryTabId ,
tabGroup . spaceId ,
newPos
);
} else if ( newPos !== undefined ) {
moveTab ( sourceData . primaryTabId , newPos );
}
}
});
Drop Indicator
Visual feedback during drag:
{ closestEdge === "top" && (
< div className = "absolute top-0 left-0 right-0 -translate-y-1/2 z-elevated" >
< DropIndicator isSpaceLight = { isSpaceLight } />
</ div >
)}
{ closestEdge === "bottom" && (
< div className = "absolute bottom-0 left-0 right-0 translate-y-1/2 z-elevated" >
< DropIndicator isSpaceLight = { isSpaceLight } />
</ div >
)}
DropIndicator Component (drop-indicator.tsx):
export function DropIndicator ({ isSpaceLight } : { isSpaceLight : boolean }) {
return (
< div className = { cn (
"h-0.5 rounded-full" ,
isSpaceLight ? "bg-black/30" : "bg-white/30"
) } />
);
}
Animation System
Layout Animations
Tab groups use Framer Motion for smooth transitions:
< motion.div
layout = "position"
initial = { { opacity: 0 , height: 0 } }
animate = { {
opacity: 1 ,
height: "auto" ,
transitionEnd: { overflow: "visible" }
} }
exit = { { opacity: 0 , height: 0 , overflow: "hidden" } }
transition = { {
layout: { type: "spring" , stiffness: 500 , damping: 35 },
height: { type: "tween" , duration: 0.2 , ease: "easeOut" },
opacity: { duration: 0.15 }
} }
>
{ tabs . map (( tab ) => (
< SidebarTab key = { tab . id } tab = { tab } isFocused = { isFocused } />
)) }
</ motion.div >
Tab Press Animation
< motion.div
whileTap = { { scale: 0.99 } }
transition = { { scale: { type: "spring" , stiffness: 600 , damping: 20 } } }
>
{ /* Tab content */ }
</ motion.div >
Flow API Integration
Tab Operations
// Switch to a tab
flow . tabs . switchToTab ( tabId : number ): void
// Close a tab
flow . tabs . closeTab ( tabId : number ): void
// Toggle mute state
flow . tabs . setTabMuted ( tabId : number , muted : boolean ): void
// Show context menu
flow . tabs . showContextMenu ( tabId : number ): void
// Move tab to different space
flow . tabs . moveTabToWindowSpace (
tabId : number ,
spaceId : string ,
position ?: number
): void
Tab Providers
The tab system uses React Context for state management:
import {
TabsProvider ,
useFocusedTab ,
useFocusedTabId ,
useFocusedTabLoading ,
useTabsGroups
} from "@/components/providers/tabs-provider" ;
function MyComponent () {
const focusedTab = useFocusedTab ();
const { tabGroups } = useTabsGroups ();
// Use tab data
}
Styling
Tab States
className = { cn (
"group/tab h-9 w-full rounded-lg" ,
"flex items-center gap-2 px-2" ,
"transition-[background-color]" ,
! isFocused && "hover:bg-black/10 dark:hover:bg-white/10" ,
isFocused && "bg-white/90 dark:bg-white/15"
)}
Dark Mode Support
All tab components respond to the current space theme:
const { isCurrentSpaceLight } = useSpaces ();
< div className = { cn (
"text-black dark:text-white" ,
! isCurrentSpaceLight && "dark"
) } >
{ /* Content */ }
</ div >
Stable References
The TabsProvider maintains stable tab object references:
// Tab objects only change when their data actually changes
// This allows React.memo to work effectively
const memoizedComparison = ( prev , next ) => {
return prev . tab === next . tab && prev . isFocused === next . isFocused ;
};
Effect Dependencies
Drag-and-drop effects use stable primitives:
const primaryTabId = tabs [ 0 ]?. id ;
useEffect (() => {
// Setup drag and drop
}, [ moveTab , tabGroup . id , position , primaryTabId , tabGroup . spaceId , tabGroup . profileId ]);
This prevents effect re-runs when tab array reference changes but IDs remain the same.
Browser Sidebar - Container for tab groups
Browser UI - Main layout
Tab providers in src/renderer/src/components/providers/tabs-provider.tsx