All components

Browser

Browser is a React and Tailwind CSS component from Eldora UI, licensed MIT. Copy it and paste it into your project.

Eldora UI MIT other

What it is

Browser is a React and Tailwind CSS component from Eldora UI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i react lucide-react

Licence

This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Browser

registry/eldoraui/browser.tsx
"use client"

import type React from "react"
import { useEffect, useState } from "react"
import Image from "next/image"
import {
  Battery,
  BookmarkIcon,
  ChevronLeft,
  ChevronRight,
  Download,
  Globe,
  History,
  Home,
  Lock,
  Maximize2,
  Minimize2,
  MoreHorizontal,
  Plus,
  RotateCcw,
  Search,
  Settings,
  Shield,
  Square,
  Star,
  StarOff,
  Volume2,
  Wifi,
  X,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"

interface Tab {
  id: string
  title: string
  url: string
  favicon?: string
  isActive: boolean
  isLoading: boolean
}

interface Bookmark {
  id: string
  title: string
  url: string
  favicon?: string
}

interface HistoryItem {
  id: string
  title: string
  url: string
  timestamp: Date
  favicon?: string
}

interface BrowserProps {
  image?: string
  initialUrl?: string
  initialTabs?: Partial<Tab>[]
  theme?: "light" | "dark" | "system"
  showWindowControls?: boolean
  showBookmarksBar?: boolean
  showStatusBar?: boolean
  className?: string
  enableTabManagement?: boolean
  enableBookmarks?: boolean
  enableHistory?: boolean
  enableDownloads?: boolean
  enableSettings?: boolean
  maxTabs?: number
  customBookmarks?: Bookmark[]
  customHistory?: HistoryItem[]
  onNavigate?: (url: string, tabId: string) => void
  onTabCreate?: (tab: Tab) => void
  onTabClose?: (tabId: string) => void
  onTabSwitch?: (tabId: string) => void
  onBookmarkToggle?: (url: string, isBookmarked: boolean) => void
  onDownload?: (url: string) => void
  renderContent?: (url: string, isLoading: boolean) => React.ReactNode
  customFavicons?: Record<string, string>
  openLinksInNewTab?: boolean
  autoFocusAddressBar?: boolean
  simulateLoading?: boolean
  loadingDuration?: number
}

export function Browser({
  image = "/placeholder.svg",
  initialUrl = "https://eldoraui.site",
  initialTabs,
  showWindowControls = false,
  showBookmarksBar = false,
  showStatusBar = true,
  className,
  enableTabManagement = false,
  enableBookmarks = true,
  enableHistory = true,
  enableDownloads = true,
  enableSettings = true,
  maxTabs = 10,
  customBookmarks,
  customHistory,
  onNavigate,
  onTabCreate,
  onTabClose,
  onTabSwitch,
  onBookmarkToggle,
  onDownload,
  renderContent,
  autoFocusAddressBar = false,
  simulateLoading = true,
  loadingDuration = 1000,
}: BrowserProps = {}) {
  const [tabs, setTabs] = useState<Tab[]>(() => {
    if (initialTabs && initialTabs.length > 0) {
      return initialTabs.map((tab, index) => ({
        id: tab.id || Date.now().toString() + index,
        title: tab.title || "New Tab",
        url: tab.url || initialUrl,
        favicon: tab.favicon,
        isActive: index === 0,
        isLoading: false,
      }))
    }
    return [
      {
        id: "1",
        title: "New Tab",
        url: initialUrl,
        isActive: true,
        isLoading: false,
      },
    ]
  })

  const [currentUrl, setCurrentUrl] = useState(initialUrl)
  const [inputUrl, setInputUrl] = useState(initialUrl)
  const [isSecure, setIsSecure] = useState(true)
  const [canGoBack, setCanGoBack] = useState(false)
  const [canGoForward, setCanGoForward] = useState(false)
  const [isBookmarked, setIsBookmarked] = useState(false)
  const [showBookmarks, setShowBookmarks] = useState(false)
  const [showHistory, setShowHistory] = useState(false)
  const [showSettings, setShowSettings] = useState(false)
  const [isFullscreen, setIsFullscreen] = useState(false)
  const [downloadProgress, setDownloadProgress] = useState(0)
  const [isDownloading, setIsDownloading] = useState(false)

  const [bookmarks] = useState<Bookmark[]>(
    customBookmarks || [
      {
        id: "1",
        title: "Google",
        url: "https://www.google.com",
        favicon: "🔍",
      },
      { id: "2", title: "GitHub", url: "https://github.com", favicon: "🐙" },
      {
        id: "3",
        title: "Stack Overflow",
        url: "https://stackoverflow.com",
        favicon: "📚",
      },
      {
        id: "4",
        title: "MDN Web Docs",
        url: "https://developer.mozilla.org",
        favicon: "📖",
      },
    ]
  )

  const [history] = useState<HistoryItem[]>(
    customHistory || [
      {
        id: "1",
        title: "Google",
        url: "https://www.google.com",
        timestamp: new Date(Date.now() - 3600000),
        favicon: "🔍",
      },
      {
        id: "2",
        title: "GitHub",
        url: "https://github.com",
        timestamp: new Date(Date.now() - 7200000),
        favicon: "🐙",
      },
      {
        id: "3",
        title: "Stack Overflow",
        url: "https://stackoverflow.com",
        timestamp: new Date(Date.now() - 10800000),
        favicon: "📚",
      },
    ]
  )

  const activeTab = tabs.find((tab) => tab.isActive)

  useEffect(() => {
    if (autoFocusAddressBar) {
      const addressBar = document.querySelector(
        'input[placeholder*="Search or enter address"]'
      ) as HTMLInputElement
      if (addressBar) {
        addressBar.focus()
      }
    }
  }, [autoFocusAddressBar])

  const createNewTab = () => {
    if (tabs.length >= maxTabs) return

    const newTab: Tab = {
      id: Date.now().toString(),
      title: "New Tab",
      url: "about:blank",
      isActive: true,
      isLoading: false,
    }

    setTabs((prev) =>
      prev.map((tab) => ({ ...tab, isActive: false })).concat(newTab)
    )
    setCurrentUrl("about:blank")
    setInputUrl("")

    onTabCreate?.(newTab)
  }

  const closeTab = (tabId: string) => {
    if (tabs.length === 1) return

    const tabIndex = tabs.findIndex((tab) => tab.id === tabId)
    const wasActive = tabs[tabIndex].isActive

    const newTabs = tabs.filter((tab) => tab.id !== tabId)

    if (wasActive && newTabs.length > 0) {
      const nextActiveIndex = Math.min(tabIndex, newTabs.length - 1)
      newTabs[nextActiveIndex].isActive = true
      setCurrentUrl(newTabs[nextActiveIndex].url)
      setInputUrl(newTabs[nextActiveIndex].url)
    }

    setTabs(newTabs)

    onTabClose?.(tabId)
  }

  const switchTab = (tabId: string) => {
    const newTabs = tabs.map((tab) => ({
      ...tab,
      isActive: tab.id === tabId,
    }))

    const activeTab = newTabs.find((tab) => tab.isActive)
    if (activeTab) {
      setCurrentUrl(activeTab.url)
      setInputUrl(activeTab.url)
    }

    setTabs(newTabs)

    onTabSwitch?.(tabId)
  }

  const navigateToUrl = (url: string) => {
    if (
      !url.startsWith("http://") &&
      !url.startsWith("https://") &&
      !url.startsWith("about:")
    ) {
      url = `https://www.google.com/search?q=${encodeURIComponent(url)}`
    }

    setCurrentUrl(url)
    setInputUrl(url)
    setIsSecure(url.startsWith("https://"))

    setTabs((prev) =>
      prev.map((tab) =>
        tab.isActive
          ? {
              ...tab,
              url,
              title: new URL(url).hostname || "New Tab",
              isLoading: simulateLoading,
            }
          : tab
      )
    )

    const activeTabId = tabs.find((tab) => tab.isActive)?.id || ""
    onNavigate?.(url, activeTabId)

    if (simulateLoading) {
      setTimeout(() => {
        setTabs((prev) =>
          prev.map((tab) => (tab.isActive ? { ...tab, isLoading: false } : tab))
        )
      }, loadingDuration)
    }
  }

  const handleUrlSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    navigateToUrl(inputUrl)
  }

  const goBack = () => {
    setCanGoForward(true)
  }

  const goForward = () => {
    setCanGoBack(true)
  }

  const refresh = () => {
    setTabs((prev) =>
      prev.map((tab) => (tab.isActive ? { ...tab, isLoading: true } : tab))
    )

    setTimeout(() => {
      setTabs((prev) =>
        prev.map((tab) => (tab.isActive ? { ...tab, isLoading: false } : tab))
      )
    }, 1000)
  }

  const toggleBookmark = () => {
    const newBookmarkedState = !isBookmarked
    setIsBookmarked(newBookmarkedState)

    onBookmarkToggle?.(currentUrl, newBookmarkedState)
  }

  const simulateDownload = () => {
    onDownload?.(currentUrl)

    if (!enableDownloads) return

    setIsDownloading(true)
    setDownloadProgress(0)

    const interval = setInterval(() => {
      setDownloadProgress((prev) => {
        if (prev >= 100) {
          clearInterval(interval)
          setIsDownloading(false)
          return 0
        }
        return prev + 10
      })
    }, 200)
  }

  return (
    <div
      className={cn(
        "bg-background border-border flex h-full flex-col overflow-hidden rounded-lg border",
        isFullscreen && "fixed inset-0 z-50 rounded-none border-0",
        className
      )}
    >
      {showWindowControls && (
        <div className="bg-muted/50 border-border flex items-center justify-between border-b px-4 py-2">
          <div className="flex items-center gap-2">
            <div className="flex gap-2">
              <div className="h-3 w-3 rounded-full bg-red-500"></div>
              <div className="h-3 w-3 rounded-full bg-yellow-500"></div>
              <div className="h-3 w-3 rounded-full bg-green-500"></div>
            </div>
          </div>

          <div className="text-muted-foreground flex items-center gap-2 text-sm">
            <Wifi className="h-4 w-4" />
            <Volume2 className="h-4 w-4" />
            <Battery className="h-4 w-4" />
            <span>
              {new Date().toLocaleTimeString([], {
                hour: "2-digit",
                minute: "2-digit",
              })}
            </span>
          </div>

          <div className="flex items-center gap-1">
            <Button
              variant="ghost"
              size="sm"
              onClick={() => setIsFullscreen(!isFullscreen)}
            >
              {isFullscreen ? (
                <Minimize2 className="h-4 w-4" />
              ) : (
                <Maximize2 className="h-4 w-4" />
              )}
            </Button>
            <Button variant="ghost" size="sm">
              <Square className="h-4 w-4" />
            </Button>
            <Button variant="ghost" size="sm">
              <X className="h-4 w-4" />
            </Button>
          </div>
        </div>
      )}

      {enableTabManagement && (
        <div className="bg-muted/30 border-border flex items-center border-b">
          <div className="flex flex-1 items-center overflow-x-auto">
            {tabs.map((tab) => (
              <div
                key={tab.id}
                className={cn(
                  "border-border flex max-w-64 min-w-0 cursor-pointer items-center gap-2 border-r px-4 py-2",
                  tab.isActive ? "bg-background" : "hover:bg-muted/50"
                )}
                onClick={() => switchTab(tab.id)}
              >
                <div className="flex min-w-0 flex-1 items-center gap-2">
                  {tab.isLoading ? (
                    <div className="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
                  ) : (
                    <Globe className="text-muted-foreground h-4 w-4 flex-shrink-0" />
                  )}
                  <span className="truncate text-sm">{tab.title}</span>
                </div>
                {tabs.length > 1 && (
                  <Button
                    variant="ghost"
                    size="sm"
                    className="hover:bg-muted h-4 w-4 p-0"
                    onClick={(e) => {
                      e.stopPropagation()
                      closeTab(tab.id)
                    }}
                  >
                    <X className="h-3 w-3" />
                  </Button>
                )}
              </div>
            ))}
          </div>

          <Button
            variant="ghost"
            size="sm"
            onClick={createNewTab}
            className="border-border border-l px-3 py-2"
          >
            <Plus className="h-4 w-4" />
          </Button>
        </div>
      )}

      <div className="bg-background border-border flex items-center gap-2 border-b p-2">
        <div className="flex items-center gap-1">
          <Button
            variant="ghost"
            size="sm"
            onClick={goBack}
            disabled={!canGoBack}
          >
            <ChevronLeft className="h-4 w-4" />
          </Button>
          <Button
            variant="ghost"
            size="sm"
            onClick={goForward}
            disabled={!canGoForward}
          >
            <ChevronRight className="h-4 w-4" />
          </Button>
          <Button variant="ghost" size="sm" onClick={refresh}>
            <RotateCcw className="h-4 w-4" />
          </Button>
          <Button
            variant="ghost"
            size="sm"
            onClick={() => navigateToUrl("about:home")}
          >
            <Home className="h-4 w-4" />
          </Button>
        </div>

        <form onSubmit={handleUrlSubmit} className="flex flex-1 items-center">
          <div className="relative flex flex-1 items-center">
            <div className="absolute left-3 flex items-center gap-2">
              {isSecure ? (
                <Lock className="h-4 w-4 text-green-600" />
              ) : (
                <Shield className="text-muted-foreground h-4 w-4" />
              )}
            </div>
            <Input
              value={inputUrl}
              onChange={(e) => setInputUrl(e.target.value)}
              placeholder="Search or enter address"
              className="pr-4 pl-10"
            />
          </div>
        </form>

        <div className="flex items-center gap-1">
          {enableBookmarks && (
            <Button variant="ghost" size="sm" onClick={toggleBookmark}>
              {isBookmarked ? (
                <Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
              ) : (
                <StarOff className="h-4 w-4" />
              )}
            </Button>
          )}
          {enableDownloads && (
            <Button variant="ghost" size="sm" onClick={simulateDownload}>
              <Download className="h-4 w-4" />
            </Button>
          )}
          {enableSettings && (
            <Button
              variant="ghost"
              size="sm"
              onClick={() => setShowSettings(!showSettings)}
            >
              <MoreHorizontal className="h-4 w-4" />
            </Button>
          )}
        </div>
      </div>

      {showBookmarksBar && enableBookmarks && (
        <div className="bg-muted/20 border-border flex items-center gap-2 border-b px-4 py-1 text-sm">
          <Button
            variant="ghost"
            size="sm"
            onClick={() => setShowBookmarks(!showBookmarks)}
            className="text-xs"
          >
            <BookmarkIcon className="mr-1 h-3 w-3" />
            Bookmarks
          </Button>
          {enableHistory && (
            <Button
              variant="ghost"
              size="sm"
              onClick={() => setShowHistory(!showHistory)}
              className="text-xs"
            >
              <History className="mr-1 h-3 w-3" />
              History
            </Button>
          )}
          <Separator orientation="vertical" className="h-4" />
          {bookmarks.slice(0, 4).map((bookmark) => (
            <Button
              key={bookmark.id}
              variant="ghost"
              size="sm"
              onClick={() => navigateToUrl(bookmark.url)}
              className="text-xs"
            >
              <span className="mr-1">{bookmark.favicon}</span>
              {bookmark.title}
            </Button>
          ))}
        </div>
      )}

      {isDownloading && enableDownloads && (
        <div className="border-border border-b bg-blue-50 px-4 py-2 dark:bg-blue-950">
          <div className="flex items-center gap-2 text-sm">
            <Download className="h-4 w-4" />
            <span>Downloading file...</span>
            <div className="bg-muted h-2 flex-1 rounded-full">
              <div
                className="h-2 rounded-full bg-blue-600 transition-all duration-200"
                style={{ width: `${downloadProgress}%` }}
              />
            </div>
            <span>{downloadProgress}%</span>
          </div>
        </div>
      )}

      <div className="flex flex-1 overflow-hidden">
        {showBookmarks && enableBookmarks && (
          <Card className="m-2 mr-0 w-80 overflow-y-auto p-4">
            <h3 className="mb-4 flex items-center gap-2 font-semibold">
              <BookmarkIcon className="h-4 w-4" />
              Bookmarks
            </h3>
            <div className="space-y-2">
              {bookmarks.map((bookmark) => (
                <div
                  key={bookmark.id}
                  className="hover:bg-muted flex cursor-pointer items-center gap-2 rounded p-2"
                  onClick={() => navigateToUrl(bookmark.url)}
                >
                  <span>{bookmark.favicon}</span>
                  <div className="min-w-0 flex-1">
                    <div className="truncate text-sm font-medium">
                      {bookmark.title}
                    </div>
                    <div className="text-muted-foreground truncate text-xs">
                      {bookmark.url}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </Card>
        )}

        {showHistory && enableHistory && (
          <Card className="m-2 mr-0 w-80 overflow-y-auto p-4">
            <h3 className="mb-4 flex items-center gap-2 font-semibold">
              <History className="h-4 w-4" />
              History
            </h3>
            <div className="space-y-2">
              {history.map((item) => (
                <div
                  key={item.id}
                  className="hover:bg-muted flex cursor-pointer items-center gap-2 rounded p-2"
                  onClick={() => navigateToUrl(item.url)}
                >
                  <span>{item.favicon}</span>
                  <div className="min-w-0 flex-1">
                    <div className="truncate text-sm font-medium">
                      {item.title}
                    </div>
                    <div className="text-muted-foreground truncate text-xs">
                      {item.url}
                    </div>
                    <div className="text-muted-foreground text-xs">
                      {item.timestamp.toLocaleTimeString()}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </Card>
        )}

        {showSettings && enableSettings && (
          <Card className="m-2 mr-0 w-80 overflow-y-auto p-4">
            <h3 className="mb-4 flex items-center gap-2 font-semibold">
              <Settings className="h-4 w-4" />
              Settings
            </h3>
            <div className="space-y-4">
              <div>
                <h4 className="mb-2 font-medium">Privacy & Security</h4>
                <div className="space-y-2 text-sm">
                  <div className="flex items-center justify-between">
                    <span>Block pop-ups</span>
                    <Badge variant="secondary">On</Badge>
                  </div>
                  <div className="flex items-center justify-between">
                    <span>Safe browsing</span>
                    <Badge variant="secondary">Enhanced</Badge>
                  </div>
                </div>
              </div>
              <Separator />
              <div>
                <h4 className="mb-2 font-medium">Appearance</h4>
                <div className="space-y-2 text-sm">
                  <div className="flex items-center justify-between">
                    <span>Theme</span>
                    <Badge variant="outline">System</Badge>
                  </div>
                  <div className="flex items-center justify-between">
                    <span>Zoom</span>
                    <Badge variant="outline">100%</Badge>
                  </div>
                </div>
              </div>
            </div>
          </Card>
        )}

        <div className="dark:bg-muted/20 border-border m-2 flex flex-1 flex-col overflow-hidden rounded-md border bg-white">
          {renderContent ? (
            renderContent(currentUrl, activeTab?.isLoading || false)
          ) : currentUrl === "about:blank" || currentUrl === "" ? (
            <div className="flex flex-1 items-center justify-center">
              <div className="space-y-4 text-center">
                <Search className="text-muted-foreground mx-auto h-16 w-16" />
                <h2 className="text-2xl font-semibold">New Tab</h2>
                <p className="text-muted-foreground">
                  Start by searching or entering a web address
                </p>
                <div className="mt-8 grid grid-cols-2 gap-4">
                  {bookmarks.slice(0, 4).map((bookmark) => (
                    <Card
                      key={bookmark.id}
                      className="hover:bg-muted/50 cursor-pointer p-4 transition-colors"
                      onClick={() => navigateToUrl(bookmark.url)}
                    >
                      <div className="space-y-2 text-center">
                        <div className="text-2xl">{bookmark.favicon}</div>
                        <div className="text-sm font-medium">
                          {bookmark.title}
                        </div>
                      </div>
                    </Card>
                  ))}
                </div>
              </div>
            </div>
          ) : (
            <div className="flex h-full items-center justify-center">
              <Image
                src={image}
                alt={image}
                width={320}
                height={320}
                className="h-full w-full rounded-md object-cover"
              />
            </div>
          )}
        </div>
      </div>

      {showStatusBar && (
        <div className="bg-muted/30 border-border text-muted-foreground flex items-center justify-between border-t px-4 py-1 text-xs">
          <div className="flex items-center gap-4">
            <span>Ready</span>
            {isSecure && (
              <span className="flex items-center gap-1">
                <Lock className="h-3 w-3" /> Secure
              </span>
            )}
          </div>
          <div className="flex items-center gap-4">
            <span>Zoom: 100%</span>
            <span>
              {tabs.length} tab{tabs.length !== 1 ? "s" : ""}
            </span>
          </div>
        </div>
      )}
    </div>
  )
}

Similar components