Navigation

v0.0.4
Media

Device

The body of a phone around a screenshot, a video or a running demo.

On this page

A device is the body of a phone around a screenshot, a video or a running demo. It is drawn in one material, like a model cast in clay: a lifted sheet for the body, a narrow bezel, and a small well at the top of the screen where the camera would be. The frame says "this is a phone" and leaves every color to what it shows, as a window does for a desktop.

Device

<div class="device" style="--device-width: 13.5rem">  <img class="device-screen" src="/demo/moonstone.svg" alt="A pale stone over grey hills"></div>

#The screen

device-screen goes on whatever the phone shows: an <img>, a <video>, an <iframe> or a block of markup. It fills the body inside the bezel, and an image is cropped to it. Its corner is the body's corner less the bezel, so the gap between the two keeps one width around the bend.

A screen made of markup starts under the camera's well: leave about a sixth of the body's width clear at the top, as a real status bar does.

Markup as a screen

<div class="device" style="--device-width: 13.5rem">  <div class="device-screen bg-surface-base flex flex-col gap-sm px(14) pt(44) pb(14)">    <p class="eyebrow">Wednesday</p>    <p class="heading heading-lg">Today</p>    <ul class="checklist mt(4)">      <li class="checklist-item">        <label class="checklist-label">          <input class="checkbox" type="checkbox" checked>          <span class="checklist-title">Review the contract</span>        </label>      </li>      <li class="checklist-item">        <label class="checklist-label">          <input class="checkbox" type="checkbox">          <span class="checklist-title">Call Grace at three</span>        </label>      </li>      <li class="checklist-item">        <label class="checklist-label">          <input class="checkbox" type="checkbox">          <span class="checklist-title">Book the flight</span>        </label>      </li>    </ul>  </div></div>

#Size

The body is 280px wide and 9 by 19.5. Resize it with --device-width, not with a width: the corner is 14% of that property and the bezel 3%, so both follow and the phone keeps its shape at any size. --device-ratio changes the proportions, for an older phone or a small tablet. The side buttons stand 2px proud of the body, and the element is that much wider on each side to hold them, so a device at the edge of a scrolling region adds nothing to scroll.

Sizes

<div class="device" style="--device-width: 7rem">  <img class="device-screen" src="/demo/moonstone.svg" alt></div><div class="device" style="--device-width: 9rem">  <img class="device-screen" src="/demo/moonstone.svg" alt></div><div class="device" style="--device-width: 11rem; --device-ratio: 3 / 4">  <img class="device-screen" src="/demo/moonstone.svg" alt></div>

#Accessibility

  • The frame is decoration and is not announced. The meaning is in the screen: give an image an alt that says what the interface shows, or an empty alt when the text beside it already does.
  • A live demo inside the screen is a real, small page. Keep its targets at touch size, and make sure it can be operated with the keyboard or is hidden from it.
  • In forced colors the body and the camera's well take a system outline.

#Classes

ClassDescription
deviceThe body: a lifted sheet 280px wide and 9 by 19.5, with a narrow bezel, the camera's well and the side buttons. The buttons stand 2px proud of the body inside the element's own box, so nothing is drawn outside it
device-screenThe screen, on an image, a video, a frame or a block of markup: it fills the body inside the bezel and is finished by a corner concentric with the body's

#Custom properties

PropertyDefaultDescription
--device-width17.5remThe body's width; the corner and the bezel are measured from it
--device-ratio9 / 19.5The body's proportions
--device-radiuscalc(var(--device-width) * .14)The body's corner, as a circular radius
--device-bezelcalc(var(--device-width) * .03)Width of the frame around the screen

#Source

@master/ui/components/device.css
/* * Device: the body of a phone around a screenshot, a video or a running demo. * It is drawn in one material, like a model cast in clay: a lifted sheet for the body, a narrow bezel, and * a small well at the top of the screen where the camera would be. The frame says "this is a phone" and * leaves every color to what it shows, as a window does. * Everything is measured from the body's width: the corner is 14% of it, the bezel 3% and the screen's own * corner is the body's minus the bezel, so the gap keeps one width around the bend. The body is 280px wide * and 9 by 19.5; set --device-width to resize it, not a width, so the corners follow. * The side buttons stand 2px proud of the body, and the element is that much wider than the body on each * side to hold them: nothing is drawn outside its box, so a device at the edge of a scroller adds no scroll. */@layer components {  .device {    --device-width: 17.5rem;    --device-ratio: 9 / 19.5;    --device-radius: calc(var(--device-width) * .14);    --device-bezel: calc(var(--device-width) * .03);    --device-button: 2px;    --surface-color: var(--color-surface-raised);    position: relative;    display: block;    flex: none;    box-sizing: border-box;    /* The body and the camera's well are layers of the device, and no further back or forward than that. */    isolation: isolate;    width: calc(var(--device-width) + var(--device-button) * 2);    height: calc(var(--device-width) / (var(--device-ratio)));    margin: 0;    padding: var(--device-bezel) calc(var(--device-bezel) + var(--device-button));    /* The buttons: three on one side and one on the other, as thin slips the body covers all but 2px of. */    background:      linear-gradient(var(--color-line-strong) 0 0) 0 17% / 50% 4% no-repeat,      linear-gradient(var(--color-line-strong) 0 0) 0 26% / 50% 8% no-repeat,      linear-gradient(var(--color-line-strong) 0 0) 0 37% / 50% 8% no-repeat,      linear-gradient(var(--color-line-strong) 0 0) 100% 31% / 50% 13% no-repeat;    /* The body: a lifted sheet behind the screen. A hairline inside its edge turns the sheet into a frame with a rim. */    &::before {      content: '';      position: absolute;      z-index: -1;      inset: 0 var(--device-button);      border-radius: calc(var(--device-radius) * var(--corner-scale));      corner-shape: var(--corner-shape);      @apply --surface-sheet;      box-shadow: inset 0 0 0 1px var(--color-line-hair), var(--shadow-lifted);      pointer-events: none;    }    /* The camera's well: a pill a third of the screen wide, a little below its top edge. */    &::after {      content: '';      position: absolute;      z-index: 1;      top: calc(var(--device-bezel) + var(--device-width) * .03);      left: 50%;      width: calc(var(--device-width) * .3);      aspect-ratio: 3.4;      border-radius: 999px;      background: var(--color-surface-raised-low);      box-shadow: var(--shadow-pressed);      translate: -50% 0;      pointer-events: none;    }    @media (forced-colors: active) {      background: none;      &::before, &::after {        outline: 1px solid CanvasText;        outline-offset: -1px;      }    }  }  /* The screen: a picture, a video, a frame or a block of markup, finished by a corner concentric with the body's. */  .device-screen {    display: block;    box-sizing: border-box;    width: 100%;    height: 100%;    margin: 0;    border: 0;    border-radius: calc(var(--device-radius) * var(--corner-scale) - var(--device-bezel));    corner-shape: var(--corner-shape);    overflow: clip;    background: var(--color-surface-inset);    outline: 1px solid var(--color-line-image-strong);    outline-offset: -1px;    object-fit: cover;    @media (forced-colors: active) {      outline-color: CanvasText;    }  }}