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 ↗Code shown on screen · 46 snippets
Web Animations API code example
// 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
// 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
// Programmatic copy
copyButtonElement.addEventListener("click", (event) => {
navigator.clipboard.writeText("Plain text to copy.").then(() => {
// Successful copy
}, () => {
// Copy failed
});
}); Async Clipboard API plain text examples
// 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
<template id="format-button">
<button class="format">
<span class="icon"></span>
<span class="label"></span>
</button>
</template> Registering the Web Component
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
<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="📋">Paste</format-button> Original example Web Component template
<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
<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
#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
<div id="editor" contenteditable="true" enterkeyhint="send"></div> System font families
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
body {
font-family: system-ui;
font-family: ui-sans-serif;
} New York font family
body {
font-family: ui-serif;
} SF Mono font family
body {
font-family: ui-monospace;
} SF Rounded font family
body {
font-family: ui-rounded;
} line-break: auto
code {
line-break: auto;
} line-break: anywhere
code {
line-break: anywhere;
} Removing margins from subsequent headings
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
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
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
:is(.intro, .pullquote, #hero) + p {
text-transform: uppercase;
}
h2 + p,
h3 + p,
h4 + p,
h5 + p,
h6 + p {
text-transform: none;
} :where () resets specificity
: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
<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
<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
<img src="MexicoCity.png"> Image with size attributes
<img src="MexicoCity.png" width="560" height="747"> Respect EXIF image orientation default behavior
image-orientation: from-image; Override image orientation to use the raw image capture
image-orientation: none; HDR display CSS media query
<style>
@media only screen (dynamic-range: high) {
/* HDR-only CSS rules */
}
</style> HDR display CSS media query and JavaScript matchMedia detection
<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
<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
<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
let bigInt = BigInt(Number.MAX_SAFE_INTEGER);
// 9007199254740991n
console.log(8n / 2n);
// 4n
console.log(9n / 2n);
// 4n Nullish coalescing operator
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
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
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
// 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
// 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
a &&= b // and assignment operator
a ||= b // or assignment operator
a ??= b // nullish assignment operator Nullish coalescing approach
// Nullish coalescing approach
element.innerHTML = element.innerHTML ?? "Hello World!" Logical assignment operator
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
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
"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
"This doesn't work, and doesn't make sense".replaceAll("doesn't",
"does");
› This does work, and does make sense App Clips banner
<meta name="apple-itunes-app"
content="app-id=myAppStoreID,
app-clip-bundle-id=clipBundleID,
affiliate-data=myAffiliateData,
app-argument=myURL"> Resources
Related sessions
-
30 min -
28 min -
28 min -
16 min