Skip to content

Commit 58ff8ac

Browse files
author
Samuel Abramov
committed
feat(toggle-group): SOTA upgrade – roving tabindex, icon variants, outlined/vertical/separated/full-width (resolve #60)
- Add roving tabindex keyboard navigation (Arrow, Home/End, wrap, disabled-skip) - Add single-select (role=radiogroup + aria-checked) alongside multi-select - Add icon-only and icon+text item variants - Add outlined, vertical, separated, full-width visual variants - Add prefers-reduced-motion support - Tokenize all values via --ct-toggle-group-* control variables - Comprehensive stories with 13 interaction tests
1 parent e6da0b8 commit 58ff8ac

2 files changed

Lines changed: 853 additions & 154 deletions

File tree

components/toggle-group.css

Lines changed: 126 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,62 @@
11
/* ── Toggle Group ── */
22

33
.ct-toggle-group {
4+
--ct-toggle-group-bg: var(--color-bg-surface);
5+
--ct-toggle-group-border: var(--color-border-default);
6+
--ct-toggle-group-radius: var(--radius-control);
7+
--ct-toggle-group-item-padding-x: var(--space-5);
8+
--ct-toggle-group-item-padding-y: var(--space-3);
9+
--ct-toggle-group-item-font-size: var(--font-size-sm);
10+
--ct-toggle-group-item-line-height: var(--line-height-sm);
11+
--ct-toggle-group-item-color: var(--color-text-secondary);
12+
--ct-toggle-group-item-bg-active: var(--color-brand-primary);
13+
--ct-toggle-group-item-color-active: var(--color-text-inverse);
14+
--ct-toggle-group-item-bg-hover: var(--color-bg-muted);
15+
--ct-toggle-group-gap: 0;
16+
417
display: inline-flex;
5-
border-radius: var(--radius-control);
6-
border: var(--border-thin) solid var(--color-border-default);
7-
background: var(--color-bg-surface);
18+
gap: var(--ct-toggle-group-gap);
19+
border-radius: var(--ct-toggle-group-radius);
20+
border: var(--border-thin) solid var(--ct-toggle-group-border);
21+
background: var(--ct-toggle-group-bg);
822
overflow: hidden;
923
}
1024

25+
/* ── Item ── */
26+
1127
.ct-toggle-group__item {
1228
appearance: none;
1329
border: none;
1430
background: transparent;
15-
padding: var(--space-3) var(--space-5);
16-
color: var(--color-text-secondary);
17-
font-size: var(--font-size-sm);
31+
padding: var(--ct-toggle-group-item-padding-y) var(--ct-toggle-group-item-padding-x);
32+
color: var(--ct-toggle-group-item-color);
33+
font-size: var(--ct-toggle-group-item-font-size);
34+
line-height: var(--ct-toggle-group-item-line-height);
1835
font-weight: var(--font-weight-medium);
1936
cursor: pointer;
2037
white-space: nowrap;
38+
display: inline-flex;
39+
align-items: center;
40+
justify-content: center;
41+
gap: var(--space-2);
42+
border-inline-end: var(--border-thin) solid var(--ct-toggle-group-border);
2143
transition: background var(--duration-fast) var(--easing-standard),
2244
color var(--duration-fast) var(--easing-standard);
23-
border-inline-end: var(--border-thin) solid var(--color-border-default);
2445
}
2546

2647
.ct-toggle-group__item:last-child {
2748
border-inline-end: none;
2849
}
2950

3051
.ct-toggle-group__item:hover {
31-
background: var(--color-bg-muted);
52+
background: var(--ct-toggle-group-item-bg-hover);
3253
}
3354

55+
/* Active state – aria-pressed for multi-select, aria-checked for single-select */
3456
.ct-toggle-group__item[aria-pressed='true'],
35-
.ct-toggle-group__item--active {
36-
background: var(--color-brand-primary);
37-
color: var(--color-text-inverse);
57+
.ct-toggle-group__item[aria-checked='true'] {
58+
background: var(--ct-toggle-group-item-bg-active);
59+
color: var(--ct-toggle-group-item-color-active);
3860
}
3961

4062
.ct-toggle-group__item:focus-visible {
@@ -43,17 +65,103 @@
4365
z-index: 1;
4466
}
4567

46-
.ct-toggle-group__item:disabled {
68+
.ct-toggle-group__item:disabled,
69+
.ct-toggle-group__item[aria-disabled='true'] {
4770
opacity: var(--opacity-disabled);
4871
cursor: not-allowed;
4972
}
5073

51-
.ct-toggle-group--sm .ct-toggle-group__item {
52-
padding: var(--space-2) var(--space-4);
53-
font-size: var(--font-size-xs);
74+
/* ── Icon element (inside icon+text items) ── */
75+
76+
.ct-toggle-group__icon {
77+
width: 1em;
78+
height: 1em;
79+
display: inline-flex;
80+
align-items: center;
81+
justify-content: center;
82+
flex-shrink: 0;
83+
}
84+
85+
/* ── Icon-only item ── */
86+
87+
.ct-toggle-group__item--icon {
88+
aspect-ratio: 1;
89+
padding: var(--ct-toggle-group-item-padding-y);
90+
}
91+
92+
/* ── Size variants ── */
93+
94+
.ct-toggle-group--sm {
95+
--ct-toggle-group-item-padding-x: var(--space-4);
96+
--ct-toggle-group-item-padding-y: var(--space-2);
97+
--ct-toggle-group-item-font-size: var(--font-size-xs);
98+
--ct-toggle-group-item-line-height: var(--line-height-xs);
99+
}
100+
101+
.ct-toggle-group--lg {
102+
--ct-toggle-group-item-padding-x: var(--space-6);
103+
--ct-toggle-group-item-padding-y: var(--space-4);
104+
--ct-toggle-group-item-font-size: var(--font-size-md);
105+
--ct-toggle-group-item-line-height: var(--line-height-md);
106+
}
107+
108+
/* ── Outlined variant ── */
109+
110+
.ct-toggle-group--outlined {
111+
background: transparent;
112+
}
113+
114+
.ct-toggle-group--outlined .ct-toggle-group__item[aria-pressed='true'],
115+
.ct-toggle-group--outlined .ct-toggle-group__item[aria-checked='true'] {
116+
background: var(--color-bg-muted);
117+
color: var(--color-brand-primary);
118+
}
119+
120+
/* ── Vertical orientation ── */
121+
122+
.ct-toggle-group--vertical {
123+
flex-direction: column;
124+
}
125+
126+
.ct-toggle-group--vertical .ct-toggle-group__item {
127+
border-inline-end: none;
128+
border-block-end: var(--border-thin) solid var(--ct-toggle-group-border);
54129
}
55130

56-
.ct-toggle-group--lg .ct-toggle-group__item {
57-
padding: var(--space-4) var(--space-6);
58-
font-size: var(--font-size-md);
131+
.ct-toggle-group--vertical .ct-toggle-group__item:last-child {
132+
border-block-end: none;
133+
}
134+
135+
/* ── Separated variant ── */
136+
137+
.ct-toggle-group--separated {
138+
--ct-toggle-group-gap: var(--space-2);
139+
border: none;
140+
background: transparent;
141+
overflow: visible;
142+
}
143+
144+
.ct-toggle-group--separated .ct-toggle-group__item {
145+
border: var(--border-thin) solid var(--ct-toggle-group-border);
146+
border-radius: var(--ct-toggle-group-radius);
147+
border-inline-end: var(--border-thin) solid var(--ct-toggle-group-border);
148+
}
149+
150+
/* ── Full-width variant ── */
151+
152+
.ct-toggle-group--full {
153+
display: flex;
154+
width: 100%;
155+
}
156+
157+
.ct-toggle-group--full .ct-toggle-group__item {
158+
flex: 1;
159+
}
160+
161+
/* ── Reduced motion ── */
162+
163+
@media (prefers-reduced-motion: reduce) {
164+
.ct-toggle-group__item {
165+
transition: none;
166+
}
59167
}

0 commit comments

Comments
 (0)