Skip to content
Corsair UI
Browse the docs

CreativeNo. 116 of 145

Phone Frame

A generic phone drawn in CSS around a screenshot or any content, with an island, a notch or no cut-out, that scales with its width.

Installation

pnpm dlx shadcn@latest add @corsair-ui/phone-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

Cut-outs

notch="island", "notch" and "none". Everything scales with the width class.

Log

Next stop

Havana, 20:00

notch="island"

Log

Next stop

Havana, 20:00

notch="notch"

Log

Next stop

Havana, 20:00

notch="none"

Sizes and ratios

buttons={false}, and aspect="9 / 16" for an older phone.

Voyage

Next stop

Havana, 20:00

9 / 16

API

PropTypeDefault
notch

The cut-out at the top of the screen.

"island" | "notch" | "none""island"
aspect

Width / height of the frame, reserved at first paint.

number | string"9 / 19.5"
bezel / radius

Any CSS length; cqw units follow the frame's width.

string"3.5cqw" / "14cqw"
buttons

Decorative side buttons. They sit just outside the frame's box.

booleantrue
screenClassName

Classes for the screen.

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=phone-frame-body]]:text-primary".

  • Set the width with a class (w-64, w-72): everything inside scales with it. For a shadow that follows the rounded body, use drop-shadow-*, not shadow-*.
  • A server component with no motion, so it sits inside parallax, magnetic or tilt wrappers.
data-slot
  • phone-frame
  • phone-frame-body
  • phone-frame-button
  • phone-frame-notch
  • phone-frame-screen
State
  • data-notchcomputed
  • data-sidecomputed

Accessibility

  • The body, cut-out and buttons are decoration and hidden from screen readers; the children keep their own semantics, so give images an alt.

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
phone-frame.tsxView on GitHub
import type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/registry/default/lib/utils";

type PhoneFrameNotch = "island" | "notch" | "none";

interface PhoneFrameProps extends ComponentProps<"div"> {
  /** Cut-out at the top of the screen: a floating pill, a classic notch, or nothing. */
  notch?: PhoneFrameNotch;
  /**
   * Width / height of the whole frame, as a number (`0.5`) or a CSS ratio
   * (`"9 / 19.5"`). The frame reserves this box before anything loads.
   */
  aspect?: number | string;
  /**
   * Space between the body edge and the screen. Any CSS length; `cqw` units
   * are relative to the frame's width, so the default scales with it.
   */
  bezel?: string;
  /** Corner radius of the body. The screen's radius is this minus the bezel. */
  radius?: string;
  /** Draw the decorative side buttons on the edges of the body. */
  buttons?: boolean;
  /** Classes for the screen element that holds the children. */
  screenClassName?: string;
}

/** Decorative side buttons: [side, top, height], in percentages of the frame height. */
const BUTTONS = [
  ["left", "18%", "5%"],
  ["left", "26%", "9%"],
  ["left", "37%", "9%"],
  ["right", "28%", "13%"],
] as const;

/**
 * A generic phone drawn in CSS around a screenshot or any content. Set the
 * width with a class (`w-64`, `w-72`…): the bezel, corners and cut-out are in
 * container units, so they scale with it, and the height follows `aspect`, so
 * the frame takes its final size at first paint. The body, cut-out and side
 * buttons are decoration hidden from screen readers; the children keep their
 * own semantics, so give images their `alt`. A server component with no
 * motion; it works inside parallax, magnetic or tilt wrappers. For a shadow
 * that follows the rounded body, use a `drop-shadow-*` class.
 *
 * @example
 * <PhoneFrame className="w-72 drop-shadow-xl">
 *   <Image src="/screens/home.png" alt="The home screen" fill className="object-cover" />
 * </PhoneFrame>
 */
function PhoneFrame({
  notch = "island",
  aspect = "9 / 19.5",
  bezel = "3.5cqw",
  radius = "14cqw",
  buttons = true,
  screenClassName,
  className,
  style,
  children,
  ...props
}: PhoneFrameProps) {
  return (
    <div
      data-slot="phone-frame"
      data-notch={notch}
      className={cn("[container-type:inline-size] relative isolate w-64 shrink-0", className)}
      style={
        {
          aspectRatio: String(aspect),
          "--phone-frame-bezel": bezel,
          "--phone-frame-radius": radius,
          ...style,
        } as CSSProperties
      }
      {...props}
    >
      {buttons &&
        BUTTONS.map(([side, top, height]) => (
          <span
            key={`${side}-${top}`}
            aria-hidden="true"
            data-slot="phone-frame-button"
            data-side={side}
            className={cn(
              "bg-border pointer-events-none absolute w-[1.2cqw]",
              side === "left" ? "left-[-1cqw] rounded-l-[1cqw]" : "right-[-1cqw] rounded-r-[1cqw]"
            )}
            style={{ top, height }}
          />
        ))}
      <span
        aria-hidden="true"
        data-slot="phone-frame-body"
        className="bg-card border-border ring-foreground/10 pointer-events-none absolute inset-0 rounded-[var(--phone-frame-radius)] border ring-1 ring-inset"
      />
      <div
        data-slot="phone-frame-screen"
        className={cn(
          "bg-background absolute inset-[var(--phone-frame-bezel)] overflow-hidden",
          "rounded-[max(0px,calc(var(--phone-frame-radius)_-_var(--phone-frame-bezel)))]",
          screenClassName
        )}
      >
        {children}
        {notch === "island" && (
          <span
            aria-hidden="true"
            data-slot="phone-frame-notch"
            className="bg-card ring-border pointer-events-none absolute top-[2.5cqw] left-1/2 z-10 h-[8cqw] w-[28cqw] -translate-x-1/2 rounded-full ring-1"
          />
        )}
        {notch === "notch" && (
          <span
            aria-hidden="true"
            data-slot="phone-frame-notch"
            className="bg-card border-border pointer-events-none absolute top-0 left-1/2 z-10 h-[6.5cqw] w-[42cqw] -translate-x-1/2 rounded-b-[4.5cqw] border border-t-0"
          />
        )}
      </div>
    </div>
  );
}

export { PhoneFrame, type PhoneFrameNotch, type PhoneFrameProps };