Skip to content
Corsair UI
Browse the docs

CreativeNo. 112 of 145

Browser Frame

A browser window drawn in CSS around a page or a screenshot, with window dots, an address field and optional actions.

Installation

pnpm dlx shadcn@latest add @corsair-ui/browser-frame

No setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.

Also adds
utils
npm
clsxtailwind-merge

Examples

Around a dashboard

The bar is decoration here and hidden from screen readers.

Berths booked

82%

Open hours

22 h

Waiting list

7

With actions and a ratio

announceUrl reads the address; aspect="16 / 10" reserves the window.

harbour.example.com/berths/12
Berth 12

Free from 20:00

API

PropTypeDefault
url

The address in the bar, as text (not a link).

string—
announceUrl

Let screen readers read the address.

booleanfalse
actions

Content at the right end of the bar.

ReactNode—
aspect

Reserve the window's box; without it the content sets the height.

number | string—
barClassName / viewportClassName

Classes for the bar and the viewport.

string—

Styling

Restyle it in your project: className on each part, the theme variables, and these attributes with arbitrary variants, e.g. className="[&_[data-slot=browser-frame-actions]]:text-primary".

data-slot
  • browser-frame
  • browser-frame-actions
  • browser-frame-address
  • browser-frame-bar
  • browser-frame-dot
  • browser-frame-dots
  • browser-frame-viewport

Accessibility

  • The window dots are always hidden from screen readers, and the address too unless announceUrl.
  • With actions the bar stays in the accessibility tree, so they can be reached.

Compatibility

Tailwind CSS
3.4 and 4, both checked in CI
React
19
Rendering
Works in server components
Motion
Respects prefers-reduced-motion

Source

1 file, installed as source you own
browser-frame.tsxView on GitHub
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/registry/default/lib/utils";

interface BrowserFrameProps extends ComponentProps<"div"> {
  /** Address shown in the bar, as plain text (it is not a link). */
  url?: string;
  /**
   * Let screen readers read the address. By default the address is
   * decoration, like the window dots, and is hidden from them.
   */
  announceUrl?: boolean;
  /** Content at the right end of the bar, such as buttons. It stays accessible. */
  actions?: ReactNode;
  /**
   * Width / height of the whole window, as a number (`16 / 10`) or a CSS ratio
   * (`"16 / 10"`). The viewport fills what the bar leaves. Without it the
   * content sets the height.
   */
  aspect?: number | string;
  /** Classes for the top bar. */
  barClassName?: string;
  /** Classes for the viewport that holds the children. */
  viewportClassName?: string;
}

/**
 * A browser window drawn in CSS around a page or a screenshot: a top bar
 * with three window dots and an address field, and a viewport for the
 * children. The dots are always hidden from screen readers; the address is
 * too unless `announceUrl` is set, and the whole bar is hidden when it holds
 * nothing to read or use. `actions` keep the bar in the accessibility tree.
 * A server component with no motion.
 *
 * @example
 * <BrowserFrame url="app.example.com/dashboard" aspect="16 / 10" className="w-full max-w-4xl">
 *   <Image src="/screens/dashboard.png" alt="The dashboard" fill className="object-cover" />
 * </BrowserFrame>
 */
function BrowserFrame({
  url,
  announceUrl = false,
  actions,
  aspect,
  barClassName,
  viewportClassName,
  className,
  style,
  children,
  ...props
}: BrowserFrameProps) {
  const hasActions = actions != null && actions !== false;
  const barHidden = !announceUrl && !hasActions;

  return (
    <div
      data-slot="browser-frame"
      className={cn(
        "border-border bg-card flex flex-col overflow-hidden rounded-lg border",
        className
      )}
      style={aspect != null ? { aspectRatio: String(aspect), ...style } : style}
      {...props}
    >
      <div
        data-slot="browser-frame-bar"
        aria-hidden={barHidden ? "true" : undefined}
        className={cn(
          "border-border flex h-11 shrink-0 items-center gap-3 border-b px-3",
          barClassName
        )}
      >
        <span
          aria-hidden="true"
          data-slot="browser-frame-dots"
          className="flex shrink-0 items-center gap-1.5"
        >
          {[0, 1, 2].map((dot) => (
            <span
              key={dot}
              data-slot="browser-frame-dot"
              className="bg-muted-foreground/40 size-2.5 rounded-full"
            />
          ))}
        </span>
        <span
          aria-hidden={announceUrl || barHidden ? undefined : "true"}
          data-slot="browser-frame-address"
          className="bg-muted text-muted-foreground flex h-7 min-w-0 flex-1 items-center rounded-md px-3 text-xs"
        >
          <span className="truncate">{url}</span>
        </span>
        {hasActions && (
          <div data-slot="browser-frame-actions" className="flex shrink-0 items-center gap-1">
            {actions}
          </div>
        )}
      </div>
      <div
        data-slot="browser-frame-viewport"
        className={cn(
          "bg-background relative overflow-hidden",
          aspect != null && "min-h-0 flex-1",
          viewportClassName
        )}
      >
        {children}
      </div>
    </div>
  );
}

export { BrowserFrame, type BrowserFrameProps };