Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2020 Safari & Web

WWDC20 · 36 min · Safari & Web

What’s new for web developers

Explore the latest features and improvements for Safari and WebKit. We’ll walk you through updated web APIs, CSS and media features, JavaScript syntax, and more to help you build great experiences for people when they use your website, home screen web apps, or embedded WebKit views.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 46 snippets

Web Animations API code example javascript · at 4:22 ↗
// Web Animations API Code Example

let needle = document.getElementById("needle");
let logo = document.getElementById("logo");
logo.addEventListener("click", () => {
    needle.animate({
        transform: [
            "rotateX(35deg) rotateZ(13deg)", 
            "rotateX(35deg) rotateZ(733deg)",
        ],
        easing: ["ease-out"],
    }, 800);
});
Resize observer example javascript · at 6:43 ↗
// Resize Observer Example

let formatPanelObserver = new ResizeObserver((entries) => {
    entries.forEach((entry) => {
        let container = entry.target;
        container.classList.toggle("small", entry.contentRect.width < 175);
   }
});

formatPanelObserver.observe(document.getElementById("format-panel"));
Async Clipboard API plain text programmatic copy javascript · at 8:15 ↗
// Programmatic copy
copyButtonElement.addEventListener("click", (event) => {
    navigator.clipboard.writeText("Plain text to copy.").then(() => {
       // Successful copy
    }, () => {
       // Copy failed
    });
});
Async Clipboard API plain text examples javascript · at 8:22 ↗
// Programmatic copy
copyButtonElement.addEventListener("click", (event) => {
    navigator.clipboard.writeText("Plain text to copy.").then(() => {
       // Successful copy
    }, () => {
       // Copy failed
    });
});

// Programmatic paste
pasteButtonElement.addEventListener("click", (event) => {
    navigator.clipboard.readText().then((clipText) => {
        document.querySelector(".editor").innerText += clipText);
    });
});
Web Component example markup xml · at 10:25 ↗
<template id="format-button">
    <button class="format">
        <span class="icon"></span>
        <span class="label"></span>
    </button>
</template>
Registering the Web Component javascript · at 10:36 ↗
let template = document.getElementById("format-button");
window.customElements.define(template.id, class extends HTMLElement {
    constructor() {
        super();

        this.attachShadow({mode: "open"});
        let newButtonElement = template.content.cloneNode(true);

        let parts = newButtonElement.querySelectorAll("span");
        parts[0].textContent = this.getAttribute("data-icon");
        parts[1].textContent = this.textContent;

        this.shadowRoot.appendChild(newButtonElement);
        this.addEventListener("click", this.handleClick.bind(this));
    }
});
Web Component custom elements xml · at 11:02 ↗
<format-button id="bold" data-icon="B">Bold</format-button>
<format-button id="italic" data-icon="I">Italic</format-button>
<format-button id="underline" data-icon="U">Underline</format-button>
<format-button id="strikethrough" data-icon="S">Strikethrough</format-button>
<format-button id="paste" data-icon="&#x1f4cb;">Paste</format-button>
Original example Web Component template xml · at 12:28 ↗
<template id="format-button">
    <button class="format">
        <span class="icon"></span>
        <span class="label"></span>
    </button>
</template>
Example Web Component template with CSS Shadow Parts xml · at 12:30 ↗
<template id="format-button">
    <button class="format">
        <span part="icon" class="icon"></span>
        <span part="label" class="label"></span>
    </button>
</template>
CSS Shadow Part styles swift · at 12:38 ↗
#bold::part(icon) {
    color: var(--formatting-button-icon-color);
    font-weight: bold;
}

#italic::part(icon) {
    color: var(--formatting-button-icon-color);
    font-style: italic;
}

#underline::part(icon) {
    color: var(--formatting-button-icon-color);
    text-decoration: underline;
}
HTML enterkeyhint attribute xml · at 13:16 ↗
<div id="editor" contenteditable="true" enterkeyhint="send"></div>
System font families swift · at 14:32 ↗
font-family: system-ui;
font-family: ui-sans-serif;
font-family: ui-serif;
font-family: ui-monospace;
font-family: ui-rounded;
San Francisco font family swift · at 14:45 ↗
body {
    font-family: system-ui;
    font-family: ui-sans-serif;
}
New York font family swift · at 14:53 ↗
body {
   font-family: ui-serif;
}
SF Mono font family swift · at 14:58 ↗
body {
   font-family: ui-monospace;
}
SF Rounded font family swift · at 15:03 ↗
body {
   font-family: ui-rounded;
}
line-break: auto swift · at 16:07 ↗
code {
    line-break: auto;
}
line-break: anywhere swift · at 16:43 ↗
code {
    line-break: anywhere;
}
Removing margins from subsequent headings swift · at 17:25 ↗
h1, h2, h3, h4, h5, h6 {
    margin-top: 3em;
}

h1 + h2,
h2 + h3,
h3 + h4,
h4 + h5,
h5 + h6 {
    margin-top: 0;
}
Removing margins from any subsequent headings swift · at 17:56 ↗
h1, h2, h3, h4, h5, h6 {
    margin-top: 3em;
}

h1 + h2, h1 + h3, h1 + h4, h1 + h5, h1 + h6,
h2 + h3, h2 + h3, h2 + h4, h2 + h5, h2 + h6,
h3 + h4, h3 + h3, h3 + h4, h3 + h5, h3 + h6,
h4 + h5, h4 + h3, h4 + h4, h4 + h5, h4 + h6,
h5 + h6, h5 + h3, h5 + h4, h5 + h5, h5 + h6 {
    margin-top: 0;
}
Using :is() to remove margins from subsequent headings swift · at 18:02 ↗
h1, h2, h3, h4, h5, h6 {
    margin-top: 3em;
}

:is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
    margin-top: 0;
}
:is() specificity prevents the override from working swift · at 18:31 ↗
:is(.intro, .pullquote, #hero) + p {
    text-transform: uppercase;
}

h2 + p,
h3 + p,
h4 + p,
h5 + p,
h6 + p {
    text-transform: none;
}
:where () resets specificity swift · at 19:07 ↗
:where(.intro, .pullquote, #hero) + p {
    text-transform: uppercase;
}
h2 + p,
h3 + p,
h4 + p,
h5 + p,
h6 + p {
    text-transform: none;
}
WebP graceful fallback to JPG xml · at 19:53 ↗
<picture>
  <source srcset="example.webp" type="image/webp">
  <img src="example.jpg" alt="Example Image">
</picture>
WebP graceful fallback to JPG and server-side detection xml · at 19:54 ↗
<picture>
  <source srcset="example.webp" type="image/webp">
  <img src="example.jpg" alt="Example Image">
</picture>

Accept: image/webp,image/png,image/svg+xml,image/*;…
Image with no size attributes xml · at 21:17 ↗
<img src="MexicoCity.png">
Image with size attributes xml · at 21:19 ↗
<img src="MexicoCity.png" width="560" height="747">
Respect EXIF image orientation default behavior swift · at 21:49 ↗
image-orientation: from-image;
Override image orientation to use the raw image capture swift · at 22:13 ↗
image-orientation: none;
HDR display CSS media query xml · at 22:37 ↗
<style>
@media only screen (dynamic-range: high) {
    /* HDR-only CSS rules */
}
</style>
HDR display CSS media query and JavaScript matchMedia detection xml · at 22:42 ↗
<style>
@media only screen (dynamic-range: high) {
    /* HDR-only CSS rules */
}
</style>

<script>
if (window.matchMedia("dynamic-range: high")) {
    // HDR-specific JavaScript
}
</script>
Remote Playback API example xml · at 23:19 ↗
<video id="videoElement" src="https://site.example/video.mp4"></video>
<button id="deviceButton">Send video to a remote device</button>

<script>
    let videoElement = document.getElementById("videoElement");
    let deviceButton = document.getElementById("deviceButton");
    deviceButton.addEventListener("click", (event) => {
        videoElement.remote.prompt().then(updateRemotePlaybackState);
    });
</script>
Picture in Picture example xml · at 24:20 ↗
<video id="videoElement" src="https://site.example/video.mp4"></video>
<button id="pipButton">Enter picture-in-picture mode</button>

<script>
    let videoElement = document.getElementById("videoElement");
    let pipButton = document.getElementById("pipButton");
    pipButton.addEventListener("click", (event) => {
        videoElement.requestPictureInPicture().then(handlePictureInPicture);
    });
</script>
BigInt example with division examples javascript · at 27:11 ↗
let bigInt = BigInt(Number.MAX_SAFE_INTEGER);
// 9007199254740991n

console.log(8n / 2n);
// 4n

console.log(9n / 2n);
// 4n
Nullish coalescing operator javascript · at 28:02 ↗
class Person {
    constructor(firstName, lastName, age) {
        this.firstName = firstName ?? "Unknown";
        this.lastName = lastName ?? "Unknown";
        this.age = age ?? NaN;
   }
}

console.log(new Person());  
// { firstName: "Unknown", lastName: "Unknown", age: NaN }

console.log(new Person(false, false, true));
// { firstName: false, lastName: false, age: true }

console.log(new Person("John", "", 0));  
// { firstName: "John", lastName: "", age: 0 }

console.log(new Person("John", "Appleseed", 42));  
// { firstName: "John", lastName: "Appleseed", age: 42 }
JavaScript optional chaining example javascript · at 29:09 ↗
class Person {
    constructor(firstName, lastName, age) {
        this.firstName = firstName ?? "Unknown";
        this.lastName = lastName ?? "Unknown";
        this.age = age ?? NaN;
        this.name = { firstName: this.firstName, lastName: this.lastName };
  }
}

function register(person) {
    // Before optional chaining
    if (person !== undefined && person.name !== undefined)
        console.log(person.name.firstName);
}

register(new Person());
// undefined

register(new Person("John", "Appleseed"));
// "John"
JavaScript optional chaining example javascript · at 29:41 ↗
class Person {
    constructor(firstName, lastName, age) {
        this.firstName = firstName ?? "Unknown";
        this.lastName = lastName ?? "Unknown";
        this.age = age ?? NaN;
        this.name = { firstName: this.firstName, lastName: this.lastName };
  }
}

function register(person) {
    // With optional chaining
    console.log(person?.name.firstName);
}

register(new Person());
􀆊 undefined

register(new Person("John", "Appleseed"));
􀆊 "John"
JavaScript optional chaining with indexes javascript · at 29:49 ↗
// Without optional chaining
console.log(person.children[0]);
// TypeError: undefined is not an object

// With optional chaining
console.log(person.children?.[0]);
// undefined
JavaScript optional chaining with methods javascript · at 30:02 ↗
// Without optional chaining
console.log(person.fullName());
􀆊 TypeError: person.fullName is not a function.

// With optional chaining
console.log(person.fullName?.());
􀆊 undefined
Logical assignment operators javascript · at 30:23 ↗
a &&= b // and assignment operator
a ||= b // or assignment operator
a ??= b // nullish assignment operator
Nullish coalescing approach javascript · at 30:44 ↗
// Nullish coalescing approach
element.innerHTML = element.innerHTML ?? "Hello World!"
Logical assignment operator swift · at 30:52 ↗
a &&= b // and assignment operator
a ||= b // or assignment operator
a ??= b // nullish assignment operator

// Nullish coalescing approach
element.innerHTML = element.innerHTML ?? "Hello World!"

// Logical assignment operator
element.innerHTML ??= "Hello World!"
Public class fields javascript · at 30:53 ↗
class Person {
    firstName = "";
    lastName = "";
    age = NaN;
    children = [];

    constructor(firstName, lastName, age) {
        this.firstName = firstName ?? "Unknown";
        this.lastName = lastName ?? "Unknown";
        this.age = age ?? NaN;
    }
}
String.prototype.replace example javascript · at 31:58 ↗
"This doesn't work, and doesn't make sense".replace ("doesn't", "does");
› This does work, and doesn't make sense
String.prototype.replaceAll example javascript · at 32:09 ↗
"This doesn't work, and doesn't make sense".replaceAll("doesn't",
"does");
› This does work, and does make sense
App Clips banner xml · at 33:53 ↗
<meta name="apple-itunes-app"
      content="app-id=myAppStoreID,
               app-clip-bundle-id=clipBundleID,
               affiliate-data=myAffiliateData,
               app-argument=myURL">

Resources