Veröffentlicht: 7. März 2025
Schaltflächen sind für dynamische Webanwendungen unerlässlich. Über Schaltflächen können Sie Menüs öffnen, Aktionen umschalten und Formulare senden. Sie bilden die Grundlage für Interaktivität im Web. Das Erstellen einfacher und barrierefreier Schaltflächen kann zu einigen überraschenden Herausforderungen führen. Entwickler, die Micro-Frontends oder Komponentensysteme erstellen, stoßen manchmal auf Lösungen, die komplexer sind als nötig. Frameworks sind zwar hilfreich, aber das Web kann hier noch mehr.
Chrome 135 führt neue Funktionen für deklaratives Verhalten mit den neuen Attributen command
und commandfor
ein. Damit werden die Attribute popovertargetaction
und popovertarget
verbessert und ersetzt. Diese neuen Attribute können Schaltflächen hinzugefügt werden, damit der Browser einige grundlegende Probleme in Bezug auf Einfachheit und Barrierefreiheit beheben und gängige Funktionen bereitstellen kann.
Traditionelle Muster
Das Erstellen von Schaltflächenverhalten ohne Framework kann bei der Entwicklung von Produktionscode einige interessante Herausforderungen mit sich bringen. HTML bietet zwar onclick
-Handler für Schaltflächen, diese sind aber aufgrund von CSP-Regeln (Content Security Policy) außerhalb von Demos oder Anleitungen oft nicht zulässig. Diese Ereignisse werden an Schaltflächenelemente gesendet. Schaltflächen werden jedoch in der Regel auf einer Seite platziert, um andere Elemente zu steuern. Dazu ist Code erforderlich, um zwei Elemente gleichzeitig zu steuern. Außerdem muss diese Interaktion für Nutzer mit Hilfstechnologien zugänglich sein. Das führt oft zu Code, der in etwa so aussieht:
<div class="menu-wrapper">
<button class="menu-opener" aria-expanded="false">
Open Menu
</button>
<div popover class="menu-content">
<!-- ... -->
</div>
</div>
<script type="module">
document.addEventListener('click', e => {
const button = e.target;
if (button.matches('.menu-opener')) {
const menu = button
.closest('.menu-wrapper')
.querySelector('.menu-content');
if (menu) {
button.setAttribute('aria-expanded', 'true');
menu.showPopover();
menu.addEventListener('toggle', e => {
// reset back to aria-expanded=false on close
if (e.newState == 'closed') {
button.setAttribute('aria-expanded', 'false');
}
}, {once: true})
}
}
});
</script>
Dieser Ansatz kann etwas instabil sein. Frameworks sollen die Ergonomie verbessern. Ein gängiges Muster bei einem Framework wie React könnte darin bestehen, den Klick einer Zustandsänderung zuzuordnen:
function MyMenu({ children }) {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), []);
const handleToggle = useCallback((e) => {
// popovers have light dismiss which influences our state
setIsOpen(e.newState === 'open')
}, []);
const popoverRef = useRef(null);
useEffect(() => {
if (popoverRef.current) {
if (isOpen) {
popoverRef.current.showPopover();
} else {
popoverRef.current.hidePopover();
}
}
}, [popoverRef, isOpen]);
return (
<>
<button onClick={open} aria-expanded={isOpen}>
Open Menu
</button>
<div popover onToggle={handleToggle} ref={popoverRef}>
{children}
</div>
</>
);
}
Viele andere Frameworks streben auch eine ähnliche Ergonomie an. In AlpineJS könnte das beispielsweise so aussehen:
<div x-data="{open: false}">
<button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
Open Menu
</button>
<div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
<!-- ... -->
</div>
</div>
In Svelte könnte das so aussehen:
<script>
let popover;
let open = false;
function togglePopover() {
open ? popover.hidePopover() : popover.showPopover();
open = !open;
}
</script>
<button on:click={togglePopover} aria-expanded={open}>
Open Menu
</button>
<div bind:this={popover} popover>
<!-- ... -->
</div>
Einige Designsysteme oder Bibliotheken gehen noch einen Schritt weiter und bieten Wrapper für Schaltflächenelemente, die die Statusänderungen umfassen. Dadurch werden Zustandsänderungen hinter einer Triggerkomponente abstrahiert. Dafür wird ein wenig Flexibilität gegen eine verbesserte Ergonomie eingetauscht:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
Das Muster „command“ und „commandfor“
Mit den Attributen command
und commandfor
können Schaltflächen jetzt deklarativ Aktionen auf anderen Elementen ausführen. So wird die Ergonomie eines Frameworks verbessert, ohne die Flexibilität zu beeinträchtigen. Für die Schaltfläche commandfor
wird eine ID verwendet, ähnlich wie beim Attribut for
. Bei command
werden dagegen vordefinierte Werte akzeptiert, was einen übertragbareren und intuitiveren Ansatz ermöglicht.
Beispiel: Eine Schaltfläche zum Öffnen eines Menüs mit „command“ und „commandfor“
Im folgenden HTML-Code werden deklarative Beziehungen zwischen der Schaltfläche und dem Menü eingerichtet, sodass der Browser die Logik und Barrierefreiheit für Sie übernimmt. Sie müssen aria-expanded
nicht verwalten oder zusätzliches JavaScript hinzufügen.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
command
und commandfor
mit popovertargetaction
und popovertarget
vergleichen
Wenn Sie popover
bereits verwendet haben, sind Ihnen die Attribute popovertarget
und popovertargetaction
möglicherweise bereits bekannt. Sie funktionieren ähnlich wie commandfor
und command
, sind aber nur für Pop-ups spezifisch. Die Attribute command
und commandfor
ersetzen diese älteren Attribute vollständig. Die neuen Attribute unterstützen alle Funktionen der älteren Attribute und bieten darüber hinaus neue Möglichkeiten.
Integrierte Befehle
Das command
-Attribut hat eine Reihe von integrierten Verhaltensweisen, die verschiedenen APIs für interaktive Elemente zugeordnet sind:
show-popover
: Wirdel.showPopover()
zugeordnet.hide-popover
: Wirdel.hidePopover()
zugeordnet.toggle-popover
: Wirdel.togglePopover()
zugeordnet.show-modal
: WirddialogEl.showModal()
zugeordnet.close
: WirddialogEl.close()
zugeordnet.
Diese Befehle werden ihren JavaScript-Entsprechungen zugeordnet. Außerdem werden die Barrierefreiheit (z. B. durch die Bereitstellung der aria-details
- und aria-expanded
-entsprechenden Beziehungen) und die Fokusverwaltung optimiert.
Beispiel: Ein Bestätigungsdialogfeld mit command
und commandfor
<button commandfor="confirm-dialog" command="show-modal">
Delete Record
</button>
<dialog id="confirm-dialog">
<header>
<h1>Delete Record?</h1>
<button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
<img role="none" src="/close-icon.svg">
</button>
</header>
<p>Are you sure? This action cannot be undone</p>
<footer>
<button commandfor="confirm-dialog" command="close" value="cancel">
Cancel
</button>
<button commandfor="confirm-dialog" command="close" value="delete">
Delete
</button>
</footer>
</dialog>
Wenn Sie auf die Schaltfläche Delete Record (Eintrag löschen) klicken, wird das Dialogfeld als modales Dialogfeld geöffnet. Wenn Sie auf die Schaltflächen Close (Schließen), Cancel (Abbrechen) oder Delete (Löschen) klicken, wird das Dialogfeld geschlossen und gleichzeitig ein "close"
-Ereignis mit einer returnValue
-Eigenschaft gesendet, die dem Wert der Schaltfläche entspricht.
Dadurch ist nur noch ein einziger Ereignis-Listener im Dialogfeld erforderlich, um zu bestimmen, was als Nächstes zu tun ist:
dialog.addEventListener("close", (event) => {
if (event.target.returnValue == "cancel") {
console.log("cancel was clicked");
} else if (event.target.returnValue == "close") {
console.log("close was clicked");
} else if (event.target.returnValue == "delete") {
console.log("delete was clicked");
}
});
Benutzerdefinierte Befehle
Zusätzlich zu den vordefinierten Befehlen können Sie benutzerdefinierte Befehle mit dem Präfix --
definieren. Benutzerdefinierte Befehle senden genau wie die vordefinierten Befehle ein "command"
-Ereignis an das Zielelement. Andernfalls wird keine zusätzliche Logik ausgeführt, wie es bei den vordefinierten Werten der Fall ist. Das bietet Flexibilität beim Erstellen von Komponenten, die auf Schaltflächen auf unterschiedliche Weise reagieren können, ohne dass es notwendig ist, Wrapper-Komponenten bereitzustellen, das DOM nach dem Zielelement zu durchsuchen oder Schaltflächenklicks Zustandsänderungen zuzuordnen. So können Sie eine API in HTML für Ihre Komponenten bereitstellen:
<button commandfor="the-image" command="--rotate-landscape">
Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
Portrait
</button>
<img id="the-image" src="photo.jpg">
<script type="module">
const image = document.getElementById("the-image");
image.addEventListener("command", (event) => {
if ( event.command == "--rotate-landscape" ) {
image.style.rotate = "-90deg"
} else if ( event.command == "--rotate-portrait" ) {
image.style.rotate = "0deg"
}
});
</script>
Befehle im ShadowDOM
Da das commandfor
-Attribut eine ID annimmt, gibt es Einschränkungen beim Überschreiten des Shadow DOM. In diesen Fällen können Sie die JavaScript API verwenden, um die Eigenschaft .commandForElement
festzulegen. Damit können Sie jedes Element über Schattenwurzeln hinweg festlegen:
<my-element>
<template shadowrootmode=open>
<button command="show-popover">Show popover</button>
<slot></slot>
</template>
<div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
const popover = this.querySelector('[popover]');
// The commandForElement can set cross-shadow root elements.
this.shadowRoot.querySelector('button').commandForElement = popover;
}
});
</script>
Künftige Vorschläge können eine deklarative Möglichkeit bieten, Referenzen über Schattengrenzen hinweg zu teilen, z. B. der Reference Target Proposal.
Nächste Schritte
Wir werden weiterhin nach Möglichkeiten für neue integrierte Befehle suchen, um gängige Funktionen von Websites abzudecken. Vorgeschlagene Ideen werden im Vorschlag für eine offene Benutzeroberfläche behandelt. Einige der bereits untersuchten Ideen:
- Öffnen und Schließen von
<details>
-Elementen - Ein
"show-picker"
-Befehl für<input>
- und<select>
-Elemente, der aufshowPicker()
zugeordnet ist. - Wiedergabebefehle für
<video>
- und<audio>
-Elemente. - Textinhalte aus Elementen kopieren
Wir freuen uns über Feedback von der Community. Wenn Sie Vorschläge haben, können Sie ein Problem im Open UI Issue Tracker melden.
Weitere Informationen
Weitere Informationen zu command
und commandfor
finden Sie in der Spezifikation und auf der MDN.