WebGL Tutorial
and more

亮色主题设计演示

撰写时间:2025-03-27

修订时间:2025-03-27

当前内容主要来自于ARIA Authoring Practices Guide (APG),其通过定义patterns的方式,规范了各类 Web 组件的设计。

边框与背景

div { --border-color: hsl(206deg 70% 50%); padding: 10px; border-radius: 5px; border: 2px solid var(--border-color); background: hsl(from var(--border-color) h s 80%); }

Hello

将边框颜色的亮度提高至80%后作为背景颜色,两种颜色色调一致,且有透明的感觉。

Breadcrumb

nav { padding: 0.8em 1em; border: 1px solid hsl(0deg 0% 90%); border-radius: 5px; background: hsl(300deg 14% 97%); & ol { margin: 0; padding-left: 0; list-style: none; & li { display: inline; & a[aria-current="page"] { color: #000; font-weight: 700; text-decoration: none; } } & li + li::before { content: ""; display: inline-block; margin: 0 0.25em; transform: rotate(15deg); border-right: 0.1em solid currentColor; height: 0.8em; } } }

Scrollable ListBox

.listbox-area { display: grid; grid-gap: 2em; grid-template-columns: repeat(2, 1fr); padding: 20px; border: 1px solid #aaa; border-radius: 4px; background: #eee; } [role="listbox"] { margin: 1em 0 0; padding: 0; min-height: 18em; border: 1px solid #aaa; background: white; } [role="listbox"]#ss_elem_list { position: relative; max-height: 18em; overflow-y: auto; } [role="listbox"] + *, .listbox-label + * { margin-top: 1em; } [role="group"] { margin: 0; padding: 0; } [role="group"] > [role="presentation"] { display: block; margin: 0; padding: 0 0.5em; font-weight: bold; line-height: 2; background-color: #ccc; } [role="option"] { position: relative; display: block; margin: 2px; padding: 2px 1em 2px 1.5em; line-height: 1.8em; cursor: pointer; } [role="listbox"]:focus [role="option"].focused { background: #bde4ff; } [role="listbox"]:focus [role="option"].focused, [role="option"]:hover { outline: 2px solid currentcolor; } .move-right-btn span.checkmark::after { content: " →"; } .move-left-btn span.checkmark::before { content: "← "; } [role="option"][aria-selected="true"] span.checkmark::before { position: absolute; left: 0.5em; content: "✓"; } button[aria-haspopup="listbox"] { position: relative; padding: 5px 10px; width: 150px; border-radius: 0; text-align: left; } button[aria-haspopup="listbox"]::after { position: absolute; right: 5px; top: 10px; width: 0; height: 0; border: 8px solid transparent; border-top-color: currentcolor; border-bottom: 0; content: ""; } button[aria-haspopup="listbox"][aria-expanded="true"]::after { position: absolute; right: 5px; top: 10px; width: 0; height: 0; border: 8px solid transparent; border-top: 0; border-bottom-color: currentcolor; content: ""; } button[aria-haspopup="listbox"] + [role="listbox"] { position: absolute; margin: 0; width: 9.5em; max-height: 10em; border-top: 0; overflow-y: auto; } [role="toolbar"] { display: flex; } [role="toolbar"] > * { border: 1px solid #aaa; background: #ccc; } [role="toolbar"] > [aria-disabled="false"]:focus { background-color: #eee; } button { font-size: inherit; } button[aria-disabled="true"] { opacity: 0.5; } .annotate { color: #366ed4; font-style: italic; } .hidden { display: none; } .offscreen { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); font-size: 14px; white-space: nowrap; }

Choose your favorite transuranic element (actinide or transactinide).

Transuranium elements:
  • None
  • Neptunium
  • Plutonium
  • Americium
  • Curium
  • Berkelium
  • Californium
  • Einsteinium
  • Fermium
  • Mendelevium
  • Nobelium
  • Lawrencium
  • Rutherfordium
  • Dubnium
  • Seaborgium
  • Bohrium
  • Hassium
  • Meitnerium
  • Darmstadtium
  • Roentgenium
  • Copernicium
  • Nihonium
  • Flerovium
  • Moscovium
  • Livermorium
  • Tennessine
  • Oganesson
/* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ 'use strict'; /** * @namespace aria * @description * The aria namespace is used to support sharing class definitions between example files * without causing eslint errors for undefined classes */ var aria = aria || {}; /** * @class * @description * Listbox object representing the state and interactions for a listbox widget * @param listboxNode * The DOM node pointing to the listbox */ aria.Listbox = class Listbox { constructor(listboxNode) { this.listboxNode = listboxNode; this.activeDescendant = this.listboxNode.getAttribute( 'aria-activedescendant' ); this.multiselectable = this.listboxNode.hasAttribute( 'aria-multiselectable' ); this.moveUpDownEnabled = false; this.siblingList = null; this.startRangeIndex = 0; this.upButton = null; this.downButton = null; this.moveButton = null; this.keysSoFar = ''; this.handleFocusChange = function () {}; this.handleItemChange = function () {}; this.registerEvents(); } registerEvents() { this.listboxNode.addEventListener('focus', this.setupFocus.bind(this)); this.listboxNode.addEventListener('keydown', this.checkKeyPress.bind(this)); this.listboxNode.addEventListener('click', this.checkClickItem.bind(this)); if (this.multiselectable) { this.listboxNode.addEventListener( 'mousedown', this.checkMouseDown.bind(this) ); } } setupFocus() { if (this.activeDescendant) { const listitem = document.getElementById(this.activeDescendant); listitem.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } else { this.focusFirstItem(); } } focusFirstItem() { var firstItem = this.listboxNode.querySelector('[role="option"]'); if (firstItem) { this.focusItem(firstItem); } } focusLastItem() { const itemList = this.listboxNode.querySelectorAll('[role="option"]'); if (itemList.length) { this.focusItem(itemList[itemList.length - 1]); } } checkKeyPress(evt) { const lastActiveId = this.activeDescendant; const allOptions = this.listboxNode.querySelectorAll('[role="option"]'); const currentItem = document.getElementById(this.activeDescendant) || allOptions[0]; let nextItem = currentItem; if (!currentItem) { return; } switch (evt.key) { case 'PageUp': case 'PageDown': evt.preventDefault(); if (this.moveUpDownEnabled) { if (evt.key === 'PageUp') { this.moveUpItems(); } else { this.moveDownItems(); } } break; case 'ArrowUp': case 'ArrowDown': evt.preventDefault(); if (!this.activeDescendant) { // focus first option if no option was previously focused, and perform no other actions this.focusItem(currentItem); break; } if (this.moveUpDownEnabled && evt.altKey) { evt.preventDefault(); if (evt.key === 'ArrowUp') { this.moveUpItems(); } else { this.moveDownItems(); } this.updateScroll(); return; } if (evt.key === 'ArrowUp') { nextItem = this.findPreviousOption(currentItem); } else { nextItem = this.findNextOption(currentItem); } if (nextItem && this.multiselectable && event.shiftKey) { this.selectRange(this.startRangeIndex, nextItem); } if (nextItem) { this.focusItem(nextItem); } break; case 'Home': evt.preventDefault(); this.focusFirstItem(); if (this.multiselectable && evt.shiftKey && evt.ctrlKey) { this.selectRange(this.startRangeIndex, 0); } break; case 'End': evt.preventDefault(); this.focusLastItem(); if (this.multiselectable && evt.shiftKey && evt.ctrlKey) { this.selectRange(this.startRangeIndex, allOptions.length - 1); } break; case 'Shift': this.startRangeIndex = this.getElementIndex(currentItem, allOptions); break; case ' ': evt.preventDefault(); this.toggleSelectItem(nextItem); break; case 'Backspace': case 'Delete': case 'Enter': if (!this.moveButton) { return; } var keyshortcuts = this.moveButton.getAttribute('aria-keyshortcuts'); if (evt.key === 'Enter' && keyshortcuts.indexOf('Enter') === -1) { return; } if ( (evt.key === 'Backspace' || evt.key === 'Delete') && keyshortcuts.indexOf('Delete') === -1 ) { return; } evt.preventDefault(); var nextUnselected = nextItem.nextElementSibling; while (nextUnselected) { if (nextUnselected.getAttribute('aria-selected') != 'true') { break; } nextUnselected = nextUnselected.nextElementSibling; } if (!nextUnselected) { nextUnselected = nextItem.previousElementSibling; while (nextUnselected) { if (nextUnselected.getAttribute('aria-selected') != 'true') { break; } nextUnselected = nextUnselected.previousElementSibling; } } this.moveItems(); if (!this.activeDescendant && nextUnselected) { this.focusItem(nextUnselected); } break; case 'A': case 'a': // handle control + A if (evt.ctrlKey || evt.metaKey) { if (this.multiselectable) { this.selectRange(0, allOptions.length - 1); } evt.preventDefault(); break; } // fall through default: if (evt.key.length === 1) { const itemToFocus = this.findItemToFocus(evt.key.toLowerCase()); if (itemToFocus) { this.focusItem(itemToFocus); } } break; } if (this.activeDescendant !== lastActiveId) { this.updateScroll(); } } findItemToFocus(character) { const itemList = this.listboxNode.querySelectorAll('[role="option"]'); let searchIndex = 0; if (!this.keysSoFar) { for (let i = 0; i < itemList.length; i++) { if (itemList[i].getAttribute('id') == this.activeDescendant) { searchIndex = i; } } } this.keysSoFar += character; this.clearKeysSoFarAfterDelay(); let nextMatch = this.findMatchInRange( itemList, searchIndex + 1, itemList.length ); if (!nextMatch) { nextMatch = this.findMatchInRange(itemList, 0, searchIndex); } return nextMatch; } /* Return the index of the passed element within the passed array, or null if not found */ getElementIndex(option, options) { const allOptions = Array.prototype.slice.call(options); // convert to array const optionIndex = allOptions.indexOf(option); return typeof optionIndex === 'number' ? optionIndex : null; } /* Return the next listbox option, if it exists; otherwise, returns null */ findNextOption(currentOption) { const allOptions = Array.prototype.slice.call( this.listboxNode.querySelectorAll('[role="option"]') ); // get options array const currentOptionIndex = allOptions.indexOf(currentOption); let nextOption = null; if (currentOptionIndex > -1 && currentOptionIndex < allOptions.length - 1) { nextOption = allOptions[currentOptionIndex + 1]; } return nextOption; } /* Return the previous listbox option, if it exists; otherwise, returns null */ findPreviousOption(currentOption) { const allOptions = Array.prototype.slice.call( this.listboxNode.querySelectorAll('[role="option"]') ); // get options array const currentOptionIndex = allOptions.indexOf(currentOption); let previousOption = null; if (currentOptionIndex > -1 && currentOptionIndex > 0) { previousOption = allOptions[currentOptionIndex - 1]; } return previousOption; } clearKeysSoFarAfterDelay() { if (this.keyClear) { clearTimeout(this.keyClear); this.keyClear = null; } this.keyClear = setTimeout( function () { this.keysSoFar = ''; this.keyClear = null; }.bind(this), 500 ); } findMatchInRange(list, startIndex, endIndex) { // Find the first item starting with the keysSoFar substring, searching in // the specified range of items for (let n = startIndex; n < endIndex; n++) { const label = list[n].innerText; if (label && label.toLowerCase().indexOf(this.keysSoFar) === 0) { return list[n]; } } return null; } checkClickItem(evt) { if (evt.target.getAttribute('role') !== 'option') { return; } this.focusItem(evt.target); this.toggleSelectItem(evt.target); this.updateScroll(); if (this.multiselectable && evt.shiftKey) { this.selectRange(this.startRangeIndex, evt.target); } } /** * Prevent text selection on shift + click for multi-select listboxes * * @param evt */ checkMouseDown(evt) { if ( this.multiselectable && evt.shiftKey && evt.target.getAttribute('role') === 'option' ) { evt.preventDefault(); } } /** * @description * Toggle the aria-selected value * @param element * The element to select */ toggleSelectItem(element) { if (this.multiselectable) { element.setAttribute( 'aria-selected', element.getAttribute('aria-selected') === 'true' ? 'false' : 'true' ); this.updateMoveButton(); } } /** * @description * Defocus the specified item * @param element * The element to defocus */ defocusItem(element) { if (!element) { return; } if (!this.multiselectable) { element.removeAttribute('aria-selected'); } element.classList.remove('focused'); } /** * @description * Focus on the specified item * @param element * The element to focus */ focusItem(element) { this.defocusItem(document.getElementById(this.activeDescendant)); if (!this.multiselectable) { element.setAttribute('aria-selected', 'true'); } element.classList.add('focused'); this.listboxNode.setAttribute('aria-activedescendant', element.id); this.activeDescendant = element.id; if (!this.multiselectable) { this.updateMoveButton(); } this.checkUpDownButtons(); this.handleFocusChange(element); } /** * Helper function to check if a number is within a range; no side effects. * * @param index * @param start * @param end * @returns {boolean} */ checkInRange(index, start, end) { const rangeStart = start < end ? start : end; const rangeEnd = start < end ? end : start; return index >= rangeStart && index <= rangeEnd; } /** * Select a range of options * * @param start * @param end */ selectRange(start, end) { // get start/end indices const allOptions = this.listboxNode.querySelectorAll('[role="option"]'); const startIndex = typeof start === 'number' ? start : this.getElementIndex(start, allOptions); const endIndex = typeof end === 'number' ? end : this.getElementIndex(end, allOptions); for (let index = 0; index < allOptions.length; index++) { const selected = this.checkInRange(index, startIndex, endIndex); allOptions[index].setAttribute('aria-selected', selected + ''); } this.updateMoveButton(); } /** * Check for selected options and update moveButton, if applicable */ updateMoveButton() { if (!this.moveButton) { return; } if (this.listboxNode.querySelector('[aria-selected="true"]')) { this.moveButton.setAttribute('aria-disabled', 'false'); } else { this.moveButton.setAttribute('aria-disabled', 'true'); } } /** * Check if the selected option is in view, and scroll if not */ updateScroll() { const selectedOption = document.getElementById(this.activeDescendant); if (selectedOption) { const scrollBottom = this.listboxNode.clientHeight + this.listboxNode.scrollTop; const elementBottom = selectedOption.offsetTop + selectedOption.offsetHeight; if (elementBottom > scrollBottom) { this.listboxNode.scrollTop = elementBottom - this.listboxNode.clientHeight; } else if (selectedOption.offsetTop < this.listboxNode.scrollTop) { this.listboxNode.scrollTop = selectedOption.offsetTop; } selectedOption.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } /** * @description * Enable/disable the up/down arrows based on the activeDescendant. */ checkUpDownButtons() { const activeElement = document.getElementById(this.activeDescendant); if (!this.moveUpDownEnabled) { return; } if (!activeElement) { this.upButton.setAttribute('aria-disabled', 'true'); this.downButton.setAttribute('aria-disabled', 'true'); return; } if (this.upButton) { if (activeElement.previousElementSibling) { this.upButton.setAttribute('aria-disabled', false); } else { this.upButton.setAttribute('aria-disabled', 'true'); } } if (this.downButton) { if (activeElement.nextElementSibling) { this.downButton.setAttribute('aria-disabled', false); } else { this.downButton.setAttribute('aria-disabled', 'true'); } } } /** * @description * Add the specified items to the listbox. Assumes items are valid options. * @param items * An array of items to add to the listbox */ addItems(items) { if (!items || !items.length) { return; } items.forEach( function (item) { this.defocusItem(item); this.toggleSelectItem(item); this.listboxNode.append(item); }.bind(this) ); if (!this.activeDescendant) { this.focusItem(items[0]); } this.handleItemChange('added', items); } /** * @description * Remove all of the selected items from the listbox; Removes the focused items * in a single select listbox and the items with aria-selected in a multi * select listbox. * @returns {Array} * An array of items that were removed from the listbox */ deleteItems() { let itemsToDelete; if (this.multiselectable) { itemsToDelete = this.listboxNode.querySelectorAll( '[aria-selected="true"]' ); } else if (this.activeDescendant) { itemsToDelete = [document.getElementById(this.activeDescendant)]; } if (!itemsToDelete || !itemsToDelete.length) { return []; } itemsToDelete.forEach( function (item) { item.remove(); if (item.id === this.activeDescendant) { this.clearActiveDescendant(); } }.bind(this) ); this.handleItemChange('removed', itemsToDelete); return itemsToDelete; } clearActiveDescendant() { this.activeDescendant = null; this.listboxNode.setAttribute('aria-activedescendant', null); this.updateMoveButton(); this.checkUpDownButtons(); } /** * @description * Shifts the currently focused item up on the list. No shifting occurs if the * item is already at the top of the list. */ moveUpItems() { if (!this.activeDescendant) { return; } const currentItem = document.getElementById(this.activeDescendant); const previousItem = currentItem.previousElementSibling; if (previousItem) { this.listboxNode.insertBefore(currentItem, previousItem); this.handleItemChange('moved_up', [currentItem]); } this.checkUpDownButtons(); } /** * @description * Shifts the currently focused item down on the list. No shifting occurs if * the item is already at the end of the list. */ moveDownItems() { if (!this.activeDescendant) { return; } var currentItem = document.getElementById(this.activeDescendant); var nextItem = currentItem.nextElementSibling; if (nextItem) { this.listboxNode.insertBefore(nextItem, currentItem); this.handleItemChange('moved_down', [currentItem]); } this.checkUpDownButtons(); } /** * @description * Delete the currently selected items and add them to the sibling list. */ moveItems() { if (!this.siblingList) { return; } var itemsToMove = this.deleteItems(); this.siblingList.addItems(itemsToMove); } /** * @description * Enable Up/Down controls to shift items up and down. * @param upButton * Up button to trigger up shift * @param downButton * Down button to trigger down shift */ enableMoveUpDown(upButton, downButton) { this.moveUpDownEnabled = true; this.upButton = upButton; this.downButton = downButton; upButton.addEventListener('click', this.moveUpItems.bind(this)); downButton.addEventListener('click', this.moveDownItems.bind(this)); } /** * @description * Enable Move controls. Moving removes selected items from the current * list and adds them to the sibling list. * @param button * Move button to trigger delete * @param siblingList * Listbox to move items to */ setupMove(button, siblingList) { this.siblingList = siblingList; this.moveButton = button; button.addEventListener('click', this.moveItems.bind(this)); } setHandleItemChange(handlerFn) { this.handleItemChange = handlerFn; } setHandleFocusChange(focusChangeHandler) { this.handleFocusChange = focusChangeHandler; } }; /* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ 'use strict'; /** * @namespace aria * @description * The aria namespace is used to support sharing class definitions between example files * without causing eslint errors for undefined classes */ var aria = aria || {}; /** * ARIA Scrollable Listbox Example * * @function onload * @description Initialize the listbox example once the page has loaded */ new aria.Listbox(document.getElementById('ss_elem_list'));

Tab

.tabs { font-family: "lucida grande", sans-serif; } [role="tablist"] { min-width: 100%; } [role="tab"], [role="tab"]:focus, [role="tab"]:hover { display: inline-block; position: relative; z-index: 2; top: 2px; margin: 0; margin-top: 4px; padding: 3px 3px 4px; border: 1px solid hsl(219deg 1% 72%); border-bottom: 2px solid hsl(219deg 1% 72%); border-radius: 5px 5px 0 0; background: hsl(220deg 20% 94%); outline: none; font-weight: bold; max-width: 22%; overflow: hidden; text-align: left; cursor: pointer; } [role="tab"][aria-selected="true"] { padding: 2px 2px 4px; margin-top: 0; border-width: 2px; border-top-width: 6px; border-top-color: rgb(36 116 214); border-bottom-color: hsl(220deg 43% 99%); background: hsl(220deg 43% 99%); } [role="tab"][aria-selected="false"] { border-bottom: 1px solid hsl(219deg 1% 72%); } [role="tab"] span.focus { display: inline-block; margin: 2px; padding: 4px 6px; } [role="tab"]:hover span.focus, [role="tab"]:focus span.focus, [role="tab"]:active span.focus { padding: 2px 4px; border: 2px solid rgb(36 116 214); border-radius: 3px; } [role="tabpanel"] { padding: 5px; border: 2px solid hsl(219deg 1% 72%); border-radius: 0 5px 5px; background: hsl(220deg 43% 99%); min-height: 10em; width: 100%; overflow: auto; } [role="tabpanel"].is-hidden { display: none; } [role="tabpanel"] p { margin: 0; }

Danish Composers

Maria Theresia Ahlefeldt (16 January 1755 – 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. Maria Theresia composed music for several ballets, operas, and plays of the royal theatre. She was given good critic as a composer and described as a “ virkelig Tonekunstnerinde ” ('a True Artist of Music').

/* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * * File: tabs-automatic.js * * Desc: Tablist widget that implements ARIA Authoring Practices */ 'use strict'; class TabsAutomatic { constructor(groupNode) { this.tablistNode = groupNode; this.tabs = []; this.firstTab = null; this.lastTab = null; this.tabs = Array.from(this.tablistNode.querySelectorAll('[role=tab]')); this.tabpanels = []; for (var i = 0; i < this.tabs.length; i += 1) { var tab = this.tabs[i]; var tabpanel = document.getElementById(tab.getAttribute('aria-controls')); tab.tabIndex = -1; tab.setAttribute('aria-selected', 'false'); this.tabpanels.push(tabpanel); tab.addEventListener('keydown', this.onKeydown.bind(this)); tab.addEventListener('click', this.onClick.bind(this)); if (!this.firstTab) { this.firstTab = tab; } this.lastTab = tab; } this.setSelectedTab(this.firstTab, false); } setSelectedTab(currentTab, setFocus) { if (typeof setFocus !== 'boolean') { setFocus = true; } for (var i = 0; i < this.tabs.length; i += 1) { var tab = this.tabs[i]; if (currentTab === tab) { tab.setAttribute('aria-selected', 'true'); tab.removeAttribute('tabindex'); this.tabpanels[i].classList.remove('is-hidden'); if (setFocus) { tab.focus(); } } else { tab.setAttribute('aria-selected', 'false'); tab.tabIndex = -1; this.tabpanels[i].classList.add('is-hidden'); } } } setSelectedToPreviousTab(currentTab) { var index; if (currentTab === this.firstTab) { this.setSelectedTab(this.lastTab); } else { index = this.tabs.indexOf(currentTab); this.setSelectedTab(this.tabs[index - 1]); } } setSelectedToNextTab(currentTab) { var index; if (currentTab === this.lastTab) { this.setSelectedTab(this.firstTab); } else { index = this.tabs.indexOf(currentTab); this.setSelectedTab(this.tabs[index + 1]); } } /* EVENT HANDLERS */ onKeydown(event) { var tgt = event.currentTarget, flag = false; switch (event.key) { case 'ArrowLeft': this.setSelectedToPreviousTab(tgt); flag = true; break; case 'ArrowRight': this.setSelectedToNextTab(tgt); flag = true; break; case 'Home': this.setSelectedTab(this.firstTab); flag = true; break; case 'End': this.setSelectedTab(this.lastTab); flag = true; break; default: break; } if (flag) { event.stopPropagation(); event.preventDefault(); } } onClick(event) { this.setSelectedTab(event.currentTarget); } } // Initialize tablist var tablists = document.querySelectorAll('[role=tablist].automatic'); for (var i = 0; i < tablists.length; i++) { new TabsAutomatic(tablists[i]); }

Toolbar

注:Toolbar是一个功能强大、设计精美,且演示了众多实用开发技巧的组件,但由于需加载太多的文件,目前正在研究如何迁移中。

参考资源

  1. ARIA Authoring Practices Guide (APG)