IPhoneDuoFrame Component
An interactive foldable iPhone frame with a cover screen and expansive inner display
iPhone DuoFoldable iPhoneDevice FrameReactUI ComponentsInteractive
September 2026 · Components
Foldable playground
5.4-inch outer display
One device. Room to unfold.
A foldable frame based on the 5.4-inch outer and 7.6-inch inner display brief. Fold it closed to show the compact cover screen, or unfold it around the center hinge to reveal one continuous inner display. Supply separate content for the cover and expanded views.
Usage
import { IPhoneDuoFrame } from "./components/ui/IPhoneDuoFrame";
<IPhoneDuoFrame
defaultOpen={false}
cover={
<img src="https://placehold.co/1080x1540" alt="Compact app view"
className="h-full w-full object-cover" />
}
>
<img src="https://placehold.co/2160x1540" alt="Expanded app view"
className="h-full w-full object-cover" />
</IPhoneDuoFrame>Copy the two source files below into your components/ui folder. Replace the sample images with screenshots, videos, or React content. The frame includes its own keyboard-accessible fold control and respects reduced motion preferences. Display proportions are illustrative.
IPhoneDuoFrame.js
import { useId, useState } from "react";
import styles from "./IPhoneDuoFrame.module.css";
/** One foldable device with a cover screen and a continuous inner display. */
export function IPhoneDuoFrame({
children,
cover,
open,
defaultOpen = true,
onOpenChange,
className = "",
}) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isOpen = open ?? internalOpen;
const screenId = useId();
function toggle() {
if (open === undefined) setInternalOpen(!isOpen);
onOpenChange?.(!isOpen);
}
return (
<div className={`${styles.frame} ${className}`} data-open={isOpen}>
<div className={styles.stage}>
<div className={styles.device}>
<div className={styles.base} aria-hidden="true" />
<div className={styles.leaf}>
<div className={styles.inside} aria-hidden="true" />
<div className={styles.outside}>
<div
className={styles.cover}
aria-hidden={isOpen}
inert={isOpen ? "" : undefined}
>
{cover}
</div>
<span className={styles.camera} aria-hidden="true" />
<span className={styles.home} aria-hidden="true" />
</div>
</div>
<div
id={screenId}
className={styles.inner}
aria-hidden={!isOpen}
inert={!isOpen ? "" : undefined}
>
{children}
<span className={styles.crease} aria-hidden="true" />
<span className={styles.home} aria-hidden="true" />
</div>
</div>
</div>
<div className={styles.controls}>
<button
type="button"
onClick={toggle}
aria-expanded={isOpen}
aria-controls={screenId}
className={styles.toggle}
>
{isOpen ? "Fold closed" : "Unfold device"}
<span aria-hidden="true">{isOpen ? "↤" : "↔"}</span>
</button>
<p className={styles.caption} aria-live="polite">
{isOpen ? "7.6-inch inner display" : "5.4-inch outer display"}
</p>
</div>
</div>
);
}
IPhoneDuoFrame.module.css
.frame {
width: 100%;
max-width: 600px;
margin-inline: auto;
container-type: inline-size;
--corner: clamp(16px, 6cqw, 36px);
--bezel: clamp(4px, 1.2cqw, 7px);
}
.stage {
padding: 12% 5%;
perspective: 1500px;
}
.device {
position: relative;
width: 100%;
aspect-ratio: 404 / 288;
transform-style: preserve-3d;
transition: transform 850ms cubic-bezier(0.22, 0.7, 0.2, 1);
}
.base,
.leaf {
position: absolute;
top: 0;
width: 50%;
height: 100%;
}
.base {
right: 0;
background: #141418;
border: 2px solid #5d5d60;
border-left: 0;
border-radius: 0 var(--corner) var(--corner) 0;
box-shadow:
2px 3px 4px #11182720,
inset -2px 0 0 #232326;
}
.leaf {
left: 0;
transform-origin: right center;
transform-style: preserve-3d;
transition: transform 850ms cubic-bezier(0.22, 0.7, 0.2, 1);
}
.inside,
.outside {
position: absolute;
inset: 0;
border: 2px solid #5d5d60;
background: #141418;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.inside {
border-right: 0;
border-radius: var(--corner) 0 0 var(--corner);
box-shadow:
-2px 3px 4px #11182720,
inset 2px 0 0 #232326;
}
.outside {
transform: rotateY(180deg) translateZ(1px);
border-radius: 5px var(--corner) var(--corner) 5px;
box-shadow: inset 0 0 0 2px #33333b;
}
.cover {
position: absolute;
inset: var(--bezel);
border-radius: 2px calc(var(--corner) - var(--bezel))
calc(var(--corner) - var(--bezel)) 2px;
overflow: hidden;
background: #222338;
}
.inner {
position: absolute;
inset: var(--bezel);
border-radius: calc(var(--corner) - var(--bezel));
background: #222338;
overflow: hidden;
transform: translateZ(2px);
opacity: 1;
visibility: visible;
transition:
opacity 220ms 620ms,
visibility 0s 620ms;
}
.frame[data-open="false"] .device {
transform: translateX(-25%);
}
.frame[data-open="false"] .leaf {
transform: rotateY(180deg);
}
.frame[data-open="false"] .inner {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 100ms,
visibility 0s 100ms;
}
.crease {
position: absolute;
inset: 0 auto 0 49%;
width: 2%;
background: linear-gradient(
90deg,
transparent,
#00000003,
#ffffff03,
transparent
);
pointer-events: none;
}
.camera {
position: absolute;
top: 6.5%;
width: 6%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 40% 30%, #38435c, #030308 60%);
box-shadow: 0 0 0 2px #08080c;
pointer-events: none;
}
.camera {
right: 8.5%;
}
.home {
position: absolute;
bottom: 1%;
left: 36%;
width: 28%;
height: clamp(2px, 0.5cqw, 3px);
border-radius: 99px;
background: #fffffff0;
pointer-events: none;
}
.outside .home {
bottom: 3.5%;
}
.controls {
text-align: center;
padding-bottom: 10px;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 24px;
padding: 12px 20px;
border-radius: 999px;
background: #18181b;
color: white;
font-size: 14px;
cursor: pointer;
}
.toggle:hover {
background: #3f3f46;
}
.toggle:focus-visible {
outline: 3px solid #8b78e6;
outline-offset: 4px;
}
.caption {
margin-top: 12px;
color: #71717a;
font-size: 12px;
}
.outside::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -5px;
width: 4px;
border-radius: 3px 0 0 3px;
background: linear-gradient(
90deg,
#414144,
#c7c7c9 45%,
#626265 80%,
#242427
);
}
.base::before,
.base::after {
content: "";
position: absolute;
top: -4px;
width: 14%;
height: 3px;
border-radius: 2px 2px 0 0;
background: linear-gradient(#9d9d9f, #333336);
}
.base::before {
right: 22%;
}
.base::after {
right: 42%;
}
@media (prefers-reduced-motion: reduce) {
.device,
.leaf,
.inner,
.frame[data-open="false"] .inner {
transition: none;
}
}
Props
children— content spanning the entire unfolded inner screen.cover— content for the outer screen when folded closed.defaultOpen— initial unfolded state. Default: true.openandonOpenChange— optional controlled state and change callback. Pass both to manage the fold state yourself.className— additional classes for the responsive wrapper.