Pure CSS Products & branding #loop #product #mockup #device

Phone mockup in pure CSS

A phone with real thickness: screen, back, four thin walls and a stack of rounded slabs that fills the corners. It turns to show both sides.

  • 181 lines of CSS
  • 18 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The screen and the back are two panels at translateZ(±depth / 2). The back is turned with rotateY(180deg) and both hide their backface, so each is only drawn while it faces you.
  2. Four thin walls close the sides. Each one only covers the straight part of its edge: it is inset by the corner radius, because a flat plane cannot follow a rounded corner.
  3. The corners are filled by nine rounded slabs, one base unit apart, stacked through the body. From any angle their edges overlap into a solid rounded rim.
  4. Every length is a multiple of that one unit, --u, so the phone — screen type included — is the same share of a gallery card, the editor and a recording canvas.
  5. The turn uses alternate with ease-in-out: it lingers on the front view and on the back view, and it is seamless without matching start and end.

Key techniques

  • front / back at ±depth/2
  • walls inset by the corner radius
  • rounded slabs plug the corners
  • alternate keyframes

Copy-paste code

HTML 18 lines

<div class="scene">
  <div class="phone">
    <i class="slab" style="--i:0"></i><i class="slab" style="--i:1"></i><i class="slab" style="--i:2"></i>
    <i class="slab" style="--i:3"></i><i class="slab" style="--i:4"></i><i class="slab" style="--i:5"></i>
    <i class="slab" style="--i:6"></i><i class="slab" style="--i:7"></i><i class="slab" style="--i:8"></i>
    <i class="wall wall-l"></i><i class="wall wall-r"></i>
    <i class="wall wall-t"></i><i class="wall wall-b"></i>
    <div class="front">
      <div class="screen">
        <span class="notch"></span>
        <strong>9:41</strong><small>Friday 18</small>
        <div class="widget"><i></i><i></i><i></i></div>
        <div class="dock"><i></i><i></i><i></i></div>
      </div>
    </div>
    <div class="back"><i></i><b></b></div>
  </div>
</div>

CSS 181 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the phone is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.36vmin;
  perspective: calc(800 * var(--u));
}

/* 84 × 168, 10 thick, 15 corner radius */
.phone {
  --metal: #5a607f;
  --shine: color-mix(in srgb, #eceefb 60%, var(--metal));
  position: relative;
  width: calc(84 * var(--u));
  height: calc(168 * var(--u));
  transform-style: preserve-3d;
  animation: turn 7s ease-in-out infinite alternate;
}

/* nine rounded slabs, one unit apart, fill the body so the corners look solid */
.slab {
  position: absolute;
  inset: calc(0.5 * var(--u));
  border-radius: calc(15 * var(--u));
  background: var(--metal);
  transform: translateZ(calc((var(--i) - 4) * var(--u)));
}

/* walls cover only the straight part of each edge: inset by the radius */
.wall {
  position: absolute;
  background: linear-gradient(90deg, var(--metal), var(--shine) 50%, var(--metal));
}

.wall-l,
.wall-r {
  left: calc(50% - 5 * var(--u));
  top: calc(15 * var(--u));
  width: calc(10 * var(--u));
  height: calc(138 * var(--u));
}

.wall-t,
.wall-b {
  left: calc(15 * var(--u));
  top: calc(50% - 5 * var(--u));
  width: calc(54 * var(--u));
  height: calc(10 * var(--u));
  background: linear-gradient(var(--metal), var(--shine) 50%, var(--metal));
}

.wall-l { transform: rotateY(-90deg) translateZ(calc(42 * var(--u))); } /* half the width */
.wall-r { transform: rotateY(90deg) translateZ(calc(42 * var(--u))); }
.wall-t { transform: rotateX(90deg) translateZ(calc(84 * var(--u))); }  /* half the height */
.wall-b { transform: rotateX(-90deg) translateZ(calc(84 * var(--u))); }

.front,
.back {
  position: absolute;
  inset: 0;
  border-radius: calc(15 * var(--u));
  backface-visibility: hidden;
}

.front {
  padding: calc(4 * var(--u)); /* the bezel */
  background: #05060c;
  transform: translateZ(calc(5 * var(--u))); /* half the thickness */
}

.screen {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  box-sizing: border-box;
  height: 100%;
  padding: calc(18 * var(--u)) calc(7 * var(--u)) calc(7 * var(--u));
  overflow: hidden;
  border-radius: calc(11 * var(--u));
  background:
    /* a wallpaper deep enough for the white clock and date: the bright #8b6cff to #ff4d9d, and the
       amber glow over the clock, made them 2.6:1 */
    radial-gradient(circle at 80% 8%, rgb(255 181 71 / 0.4), transparent 45%),
    linear-gradient(165deg, #6a45f5, #d1206f 70%, #c96f00 120%);
  color: #fff;
}

.screen strong {
  font-size: calc(21 * var(--u));
  font-weight: 800;
  line-height: 1;
  letter-spacing: calc(-0.5 * var(--u));
}

.screen small { margin-top: calc(2 * var(--u)); font-size: calc(8 * var(--u)); }

.notch {
  position: absolute;
  top: calc(5 * var(--u));
  left: calc(50% - 12 * var(--u));
  width: calc(24 * var(--u));
  height: calc(6 * var(--u));
  border-radius: calc(3 * var(--u));
  background: #05060c;
}

.widget {
  display: grid;
  gap: calc(4 * var(--u));
  box-sizing: border-box;
  width: 100%;
  margin-top: calc(12 * var(--u));
  padding: calc(7 * var(--u));
  border: calc(1 * var(--u)) solid rgb(255 255 255 / 0.35);
  border-radius: calc(8 * var(--u));
  background: rgb(255 255 255 / 0.18);
}

.widget i { height: calc(4 * var(--u)); border-radius: calc(2 * var(--u)); background: rgb(255 255 255 / 0.85); }
.widget i:nth-child(2) { width: 70%; opacity: 0.6; }
.widget i:nth-child(3) { width: 45%; opacity: 0.6; }

.dock {
  display: flex;
  gap: calc(5 * var(--u));
  justify-content: center;
  box-sizing: border-box;
  width: 100%;
  margin-top: auto;
  padding: calc(5 * var(--u));
  border-radius: calc(9 * var(--u));
  background: rgb(255 255 255 / 0.2);
}

.dock i { width: calc(13 * var(--u)); height: calc(13 * var(--u)); border-radius: calc(4 * var(--u)); background: rgb(255 255 255 / 0.9); }
.dock i:nth-child(2) { background: #2ee6d6; }
.dock i:nth-child(3) { background: #05060c; }

.back {
  /* an inner line plus a soft shade instead of a hard border: turning, the back passes
     side-on, where a one-unit line breaks up; the shade survives */
  box-shadow:
    inset 0 0 0 calc(1 * var(--u)) rgb(236 238 251 / 0.25),
    inset 0 0 calc(8 * var(--u)) rgb(236 238 251 / 0.1);
  background:
    linear-gradient(125deg, transparent 40%, rgb(255 255 255 / 0.14) 50%, transparent 60%),
    linear-gradient(160deg, #5546a2, #26254f);
  transform: rotateY(180deg) translateZ(calc(5 * var(--u)));
}

/* camera island with two lenses */
.back i {
  position: absolute;
  top: calc(9 * var(--u));
  left: calc(9 * var(--u));
  width: calc(30 * var(--u));
  height: calc(30 * var(--u));
  border-radius: calc(9 * var(--u));
  background:
    radial-gradient(circle at calc(9 * var(--u)) calc(9 * var(--u)), #4a5078 0 calc(2 * var(--u)), #05060c calc(2.5 * var(--u)) calc(6 * var(--u)), transparent calc(6.5 * var(--u))),
    radial-gradient(circle at calc(21 * var(--u)) calc(21 * var(--u)), #4a5078 0 calc(2 * var(--u)), #05060c calc(2.5 * var(--u)) calc(6 * var(--u)), transparent calc(6.5 * var(--u))),
    #2f2957;
}

/* logo */
.back b {
  position: absolute;
  top: calc(50% - 8 * var(--u));
  left: calc(50% - 8 * var(--u));
  box-sizing: border-box;
  width: calc(16 * var(--u));
  height: calc(16 * var(--u));
  border: calc(3 * var(--u)) solid rgb(255 255 255 / 0.55);
  border-radius: 50%;
}

/* 215° = 180° + 35°: the back, seen from the mirrored angle */
@keyframes turn {
  from { transform: rotateX(-8deg) rotateY(-35deg); }
  to   { transform: rotateX(-8deg) rotateY(215deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.