Before this lesson
Understand buttons, links, DOM events, focus and the hidden attribute.
What you will be able to do
Implement a two-step language picker with deliberate selection and predictable keyboard closing.
Understand the mechanism
The trigger controls visibility and option links perform navigation. Clicking the trigger only expands choices; it does not change the URL or language. Keep aria-expanded synchronized with hidden and point aria-controls at the actual container.
This is a disclosure of ordinary links, not a full application menu. Native buttons support Enter and Space, and links support Tab. Escape closes and returns focus; outside clicks or focus leaving the region also close it.
A real site preserves article ID, search parameters and fragment while replacing only lang. The simplified links demonstrate interaction; the snippet itself has no translation engine and must connect to bilingual content.
Runnable example
<div id="picker">
<button id="trigger" type="button" aria-expanded="false" aria-controls="choices">Language ▾</button>
<div id="choices" hidden><a href="?lang=zh">中文</a> <a href="?lang=en">English</a></div>
</div>
<script>
const root = document.querySelector('#picker');
const trigger = document.querySelector('#trigger');
const choices = document.querySelector('#choices');
function setOpen(open) { choices.hidden = !open; trigger.setAttribute('aria-expanded', String(open)); }
trigger.addEventListener('click', () => setOpen(choices.hidden));
document.addEventListener('click', event => { if (!root.contains(event.target)) setOpen(false); });
root.addEventListener('keydown', event => {
if (event.key === 'Escape') { setOpen(false); trigger.focus(); }
});
root.addEventListener('focusout', event => {
if (event.relatedTarget && !root.contains(event.relatedTarget)) setOpen(false);
});
</script>
Trace and verify
First click: URL unchanged and options visible. Second click: closed. Open again, Tab to an option, and Escape back to the trigger. Outside clicks and tabbing away close it. Only selecting a link navigates.
Common mistakes and boundaries
Never navigate on hover or change language when merely opening the picker. hidden removes invisible links from keyboard navigation; opacity alone does not.
Practice challenge
Construct links with URL and searchParams, preserve the hash, and mark the active language with aria-current.
Reference solution and reasoning
Copy location.href into a URL, set lang on the copy and assign href. Changing only lang preserves other parameters. Mark the current option with aria-current=true while retaining a usable link.