Skip to content

Commit 3081b33

Browse files
authored
feat(curriculum): added interactive examples to aria-controls attribute lesson (freeCodeCamp#63867)
1 parent 4bde0ce commit 3081b33

File tree

1 file changed

+179
-3
lines changed

1 file changed

+179
-3
lines changed

curriculum/challenges/english/blocks/lecture-understanding-aria-expanded-aria-live-and-common-aria-states/672a5507d857a891139abc7f.md

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ challengeType: 19
55
dashedName: what-is-the-aria-controls-attribute
66
---
77

8-
# --description--
8+
# --interactive--
99

1010
The `aria-controls` attribute is used to create a programmatic relationship between an element that controls the presence of another element on the page, and the element it controls. This relationship can help screen reader users better understand how the control works. Common uses include a set of tabs that control the visibility of different sections of content, or a button that toggles the visibility of a menu.
1111

12-
Let's take a look at an example to see how this works.
13-
In this example of a tabbed interface, we have a `div` element with a set of three buttons:
12+
Let's take a look at an example to see how this works. In this example of a tabbed interface, we have a `div` element with a set of three buttons:
13+
14+
:::interactive_editor
1415

1516
```html
17+
<link rel="stylesheet" href="styles.css">
1618
<div role="tablist">
1719
<button role="tab" id="tab1" aria-controls="section1" aria-selected="true">
1820
Tab 1
@@ -26,13 +28,73 @@ In this example of a tabbed interface, we have a `div` element with a set of thr
2628
</div>
2729
```
2830

31+
```css
32+
[role="tablist"] {
33+
display: flex;
34+
border-bottom: 2px solid #ccc;
35+
margin-bottom: 1em;
36+
gap: 4px;
37+
}
38+
39+
[role="tab"] {
40+
background: #f5f5f5;
41+
border: 1px solid #ccc;
42+
border-bottom: none;
43+
border-radius: 4px 4px 0 0;
44+
padding: 8px 16px;
45+
cursor: pointer;
46+
font-size: 14px;
47+
color: #333;
48+
transition: background 0.2s, color 0.2s;
49+
}
50+
51+
[role="tab"]:hover,
52+
[role="tab"]:focus {
53+
background: #e8f0fe;
54+
outline: none;
55+
}
56+
57+
[role="tab"][aria-selected="true"] {
58+
background: #fff;
59+
border-color: #ccc;
60+
border-bottom: 2px solid #fff;
61+
color: #000;
62+
font-weight: 600;
63+
position: relative;
64+
top: 1px;
65+
}
66+
67+
[role="tab"]:focus-visible {
68+
outline: 2px solid #0078d4;
69+
outline-offset: 2px;
70+
}
71+
72+
```
73+
74+
:::
75+
2976
The `div` uses `role="tablist"` to indicate that it serves as a container element for a group of tabs.
3077

3178
Each button represents a tab and has a `role` attribute set to `tab`. In most tabbed interfaces, the first tab panel is visible by default, so the first tab button has an `aria-selected` attribute set to `true` to indicate that its associated section of content is currently visible. The `aria-controls` attribute is used to associate each button with the section of content that it controls.
3279

3380
Here are the three sections of content that correspond to the tabs:
3481

82+
:::interactive_editor
83+
3584
```html
85+
<link rel="stylesheet" href="styles.css">
86+
<div role="tablist">
87+
<button role="tab" id="tab1" aria-controls="section1" aria-selected="true">
88+
Tab 1
89+
</button>
90+
<button role="tab" id="tab2" aria-controls="section2" aria-selected="false">
91+
Tab 2
92+
</button>
93+
<button role="tab" id="tab3" aria-controls="section3" aria-selected="false">
94+
Tab 3
95+
</button>
96+
</div>
97+
3698
<div id="section1" role="tabpanel" aria-labelledby="tab1">
3799
Section 1 content
38100
</div>
@@ -44,8 +106,122 @@ Here are the three sections of content that correspond to the tabs:
44106
<div id="section3" role="tabpanel" aria-labelledby="tab3" hidden>
45107
Section 3 content
46108
</div>
109+
<script src="index.js"></script>
110+
```
111+
112+
```css
113+
[role="tablist"] {
114+
display: flex;
115+
border-bottom: 2px solid #ccc;
116+
margin-bottom: 1em;
117+
gap: 4px;
118+
background: #fafafa;
119+
padding: 0.25em;
120+
}
121+
122+
[role="tab"] {
123+
background: #f5f5f5;
124+
border: 1px solid #ccc;
125+
border-bottom: none;
126+
border-radius: 4px 4px 0 0;
127+
padding: 8px 16px;
128+
cursor: pointer;
129+
font-size: 14px;
130+
color: #333;
131+
transition: background 0.2s, color 0.2s;
132+
}
133+
134+
[role="tab"]:hover,
135+
[role="tab"]:focus {
136+
background: #e8f0fe;
137+
outline: none;
138+
}
139+
140+
[role="tab"][aria-selected="true"] {
141+
background: #fff;
142+
border-color: #ccc;
143+
border-bottom: 2px solid #fff;
144+
color: #000;
145+
font-weight: 600;
146+
position: relative;
147+
top: 1px;
148+
}
149+
150+
[role="tab"]:focus-visible {
151+
outline: 2px solid #0078d4;
152+
outline-offset: 2px;
153+
}
154+
155+
[role="tabpanel"] {
156+
border: 1px solid #ccc;
157+
border-radius: 0 4px 4px 4px;
158+
padding: 16px;
159+
background-color: #fff;
160+
color: #333;
161+
font-size: 15px;
162+
line-height: 1.4;
163+
}
164+
165+
[role="tabpanel"][hidden] {
166+
display: none;
167+
}
168+
169+
```
170+
171+
```js
172+
const tabs = document.querySelectorAll('[role="tab"]');
173+
const tabList = document.querySelector('[role="tablist"]');
174+
175+
tabs.forEach(tab => {
176+
tab.addEventListener('click', () => {
177+
activateTab(tab);
178+
});
179+
180+
tab.addEventListener('keydown', (e) => {
181+
const key = e.key;
182+
const currentIndex = Array.from(tabs).indexOf(tab);
183+
let newIndex = null;
184+
185+
if (key === 'ArrowRight') {
186+
newIndex = (currentIndex + 1) % tabs.length;
187+
} else if (key === 'ArrowLeft') {
188+
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
189+
} else if (key === 'Home') {
190+
newIndex = 0;
191+
} else if (key === 'End') {
192+
newIndex = tabs.length - 1;
193+
}
194+
195+
if (newIndex !== null) {
196+
tabs[newIndex].focus();
197+
activateTab(tabs[newIndex]);
198+
}
199+
});
200+
});
201+
202+
function activateTab(tab) {
203+
const tabPanels = document.querySelectorAll('[role="tabpanel"]');
204+
205+
tabs.forEach(t => {
206+
t.setAttribute('aria-selected', 'false');
207+
t.setAttribute('tabindex', '-1');
208+
});
209+
210+
tabPanels.forEach(panel => panel.hidden = true);
211+
212+
tab.setAttribute('aria-selected', 'true');
213+
tab.removeAttribute('tabindex');
214+
215+
const panelId = tab.getAttribute('aria-controls');
216+
const panel = document.getElementById(panelId);
217+
panel.hidden = false;
218+
tab.focus();
219+
}
220+
47221
```
48222

223+
:::
224+
49225
Each section of content has a `role` attribute set to `tabpanel` and an `aria-labelledby` attribute that points to the corresponding tab to give the panel an accessible name. The `hidden` attribute is used to hide the sections of content that are not currently selected. Each section ID matches the value of the `aria-controls` attribute on the corresponding button. The `section1` ID matches the `aria-controls` attribute on the first button, `section2` matches the `aria-controls` attribute on the second button, and `section3` matches the `aria-controls` attribute on the third button. This is how the association between the tabs and their sections of content is established.
50226

51227
To switch between the different sections you will need to use JavaScript to update the `hidden` attribute on the section and the `aria-selected` attribute on the button based on which section is currently visible. Only one section can be visible at a time and it should not have the `hidden` attribute and `aria-selected` should be set to `true` on its button. The remaining hidden sections should all have the `hidden` attribute and `aria-selected` should be set to `false` on their buttons.

0 commit comments

Comments
 (0)