Skip to main content

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;
}

SidebarTab Component

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

tab
TabData
required
The tab data object containing title, URL, favicon, etc.
isFocused
boolean
required
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
<button
  className={cn(
    "size-5.5 rounded-sm",
    "hover:bg-black/10 dark:hover:bg-white/10",
    "opacity-0 pointer-events-none",
    "group-hover/tab:opacity-100 group-hover/tab:pointer-events-auto"
  )}
  onClick={handleCloseTab}
>
  <XIcon className="size-4.5" />
</button>
  • Visible only on tab hover
  • Prevents parent click event propagation

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

tabGroup
TabGroupType
required
The tab group object containing tabs array and metadata
isActive
boolean
required
Whether this group contains the active tab
isFocused
boolean
required
Whether this group is in the focused window/space
isSpaceLight
boolean
required
Whether the current space uses a light theme
position
number
required
Position index for drag-and-drop ordering
groupCount
number
required
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>

Performance Optimizations

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

Build docs developers (and LLMs) love