Borrador #3619


WebXR Demo

Customize a WebXR Augmented Reality session with HTML, CSS, and JS in Chrome 83+ on Android.

This demonstrates the use of slots, as well as shared and unshared DOM between the 3D and AR modes.

A slot is used here for replacing the default AR button with a custom one – in this case
the one recommended by SceneViewer. This is not the default only because this way localization of the text is left
to whatever system you prefer to use. Note the AR button will only be visible on AR-capable devices.

By styling based on the ar-status attribute, you can add DOM that only shows up in certain modes.
In this case a CSS animation has been added to prompt the user to move their phone around to help ARCore find
their floor so that the object can be placed. User studies show a prompt like this is an important part of
guiding users to a good AR experience.

Finally, even complex DOM can easily function in both 3D and AR modes, including interactions via script. In
this case a simple carousel of models is demonstrated. Unfortunately none of this DOM content can flow into
SceneViewer or QuickLook, as these are native apps. Only through WebXR, now our default AR mode, can this be
achieved, as the AR session is still inside of the browser. This also removes the need to redownload the model.

Note that by specifically including quick-look in
ar-modes, but not specifying an ios-src,
the USDZ will instead be auto-generated when the user clicks the
AR button on iOS to launch Quick Look.

<model-viewer src="" poster="" shadow-intensity="1" ar ar-modes="webxr scene-viewer quick-look" camera-controls alt="A 3D model carousel">
  <button slot="ar-button" id="ar-button">
    View in your space

  <div id="ar-prompt">
    <img src="">

  <button id="ar-failure">
    AR is not tracking!

  <div class="slider">
    <div class="slides">
      <button class="slide selected" onclick="switchSrc(this, 'Chair')" style="background-image: url('');">

      </button><button class="slide" onclick="switchSrc(this, 'Mixer')" style="background-image: url('');">

      </button><button class="slide" onclick="switchSrc(this, 'GeoPlanter')" style="background-image: url('');">
      </button><button class="slide" onclick="switchSrc(this, 'ToyTrain')" style="background-image: url('');">
      </button><button class="slide" onclick="switchSrc(this, 'Canoe')" style="background-image: url('');">    

<script type="module">
  const modelViewer = document.querySelector("model-viewer");

  window.switchSrc = (element, name) => {
    const base = "" + name;
    modelViewer.src = base + '.glb';
    modelViewer.poster = base + '.png';
    const slides = document.querySelectorAll(".slide");
    slides.forEach((element) => {element.classList.remove("selected");});

  document.querySelector(".slider").addEventListener('beforexrselect', (ev) => {
    // Keep slider interactions from affecting the XR scene.

  /* This keeps child nodes hidden while the element loads */
  :not(:defined) > * {
    display: none;

  model-viewer {
    background-color: #eee;
    overflow-x: hidden;
    --poster-color: #eee;

  #ar-button {
    background-image: url(;
    background-repeat: no-repeat;
    background-size: 20px 20px;
    background-position: 12px 50%;
    background-color: #fff;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    bottom: 132px;
    padding: 0px 16px 0px 40px;
    font-family: Roboto Regular, Helvetica Neue, sans-serif;
    font-size: 14px;
    height: 36px;
    line-height: 36px;
    border-radius: 18px;
    border: 1px solid #DADCE0;

  #ar-button:active {
    background-color: #E8EAED;

  #ar-button:focus {
    outline: none;

  #ar-button:focus-visible {
    outline: 1px solid #4285f4;

  @keyframes circle {
    from { transform: translateX(-50%) rotate(0deg) translateX(50px) rotate(0deg); }
    to   { transform: translateX(-50%) rotate(360deg) translateX(50px) rotate(-360deg); }

  @keyframes elongate {
    from { transform: translateX(100px); }
    to   { transform: translateX(-100px); }

  model-viewer > #ar-prompt {
    position: absolute;
    left: 50%;
    bottom: 175px;
    animation: elongate 2s infinite ease-in-out alternate;
    display: none;

  model-viewer[ar-status="session-started"] > #ar-prompt {
    display: block;

  model-viewer > #ar-prompt > img {
    animation: circle 4s linear infinite;

  model-viewer > #ar-failure {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 175px;
    display: none;

  model-viewer[ar-tracking="not-tracking"] > #ar-failure {
    display: block;

  .slider {
    width: 100%;
    text-align: center;
    overflow: hidden;
    position: absolute;
    bottom: 16px;

  .slides {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;

  .slide {
    scroll-snap-align: start;
    flex-shrink: 0;
    width: 100px;
    height: 100px;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    background-color: #fff;
    margin-right: 10px;
    border-radius: 10px;
    border: none;
    display: flex;

  .slide.selected {
    border: 2px solid #4285f4;

  .slide:focus {
    outline: none;

  .slide:focus-visible {
    outline: 1px solid #4285f4;


Augmented Reality

This demonstrates several augmented reality modes, including
webxr, scene-viewer,
quick-look & the accompanying attributes,
ar, ar-scale,

Note that WebXR mode requires the page be served on HTTPS and if
enclosed in an iframe, that iframe must allow your origin the


In this example an ios-src attribute is specified,
which enables Quick Look on iOS even if it is not specified in
ar-modes. This requires an extra download, but can be
useful if the auto-generated USDZ is not adequate (for instance it
does not support animations yet). Also, this source can be either
a .usdz or a .reality file.

<model-viewer src="" ar ar-scale="fixed" camera-controls alt="A 3D model of an astronaut" skybox-image="" ios-src="" xr-environment></model-viewer>

AR is not supported on this device

Scene Viewer

Here the Scene Viewer app is given priority, to make it easier to compare with the default WebXR, above.

<model-viewer id="model-viewer" src="" ar ar-modes="scene-viewer webxr" camera-controls alt="A 3D model of an astronaut" skybox-image="">
  <div id="error" class="hide">AR is not supported on this device</div>
  document.querySelector("#model-viewer").addEventListener('ar-status', (event) => {
    if(event.detail.status === 'failed'){
      const error = document.querySelector("#error");
      error.addEventListener('transitionend',(event) => {
  #error {
    background-color: #ffffffdd;
    border-radius: 16px;
    padding: 16px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate3d(-50%, -50%, 0);
    transition: opacity 0.3s;
  #error.hide {
    opacity: 0;
    visibility: hidden;
    transition: visibility 2s, opacity 1s 1s;

Placing on a Wall

This demonstrates the ar-placement attribute, which defaults to «floor», but using «wall» gives a different AR placement experience.

<model-viewer src="" ar ar-placement="wall" ar-modes="webxr scene-viewer quick-look" camera-controls alt="A 3D model of some wall art"></model-viewer>

Using Slots In <model-viewer>. This page demonstrates how you can change parts of <model-viewer> using web component slots.

Custom AR Button

<model-viewer ar ar-modes="webxr scene-viewer quick-look" camera-controls src="" alt="A 3D model of an astronaut">
  <button slot="ar-button" style="background-color: white; border-radius: 4px; border: none; position: absolute; top: 16px; right: 16px; ">
      👋 Activate AR

Since this slot will only appear on an AR enabled device screenshots are provided below. They compare the <model-viewer> default button in the bottom right and a custom button («👋 Activate AR») in the top right of the viewport, with a custom style.

Image displaying the default <model-viewer> button of a box with slits cut out in the lower-right, next to the example astronaut model.»><br />
          <img class=

is visible

Transparent Background

<div class="demo" style="background: linear-gradient(#ffffff, #ada996); overflow-x: hidden;">
  <span style="position: absolute; text-align: center; font-size: 100px; line-height: 100px; left: 50%; transform: translateX(-50%);">Background<br>is visible<br>through<br>transparent<br>objects.</span>
  <model-viewer camera-controls src="" ar ar-modes="webxr scene-viewer quick-look" alt="A 3D transparency test" style="background-color: unset;"></model-viewer>