@compa11y/react 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @compa11y/react
2
2
 
3
- Accessible React components that just work.
3
+ Accessible React components that just work. Every component handles ARIA attributes, keyboard navigation, focus management, and screen reader announcements under the hood.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,724 +10,32 @@ npm install @compa11y/react
10
10
 
11
11
  ## Components
12
12
 
13
- ### Dialog
14
-
15
- ```tsx
16
- import { Dialog } from '@compa11y/react';
17
-
18
- function ConfirmDialog({ open, onClose, onConfirm }) {
19
- return (
20
- <Dialog open={open} onOpenChange={onClose}>
21
- <Dialog.Title>Confirm Delete</Dialog.Title>
22
- <Dialog.Description>
23
- Are you sure you want to delete this item?
24
- </Dialog.Description>
25
- <Dialog.Actions>
26
- <button onClick={onClose}>Cancel</button>
27
- <button onClick={onConfirm}>Delete</button>
28
- </Dialog.Actions>
29
- </Dialog>
30
- );
31
- }
32
- ```
33
-
34
- ### Menu
35
-
36
- ```tsx
37
- import { Menu } from '@compa11y/react';
38
-
39
- function ActionMenu() {
40
- return (
41
- <Menu>
42
- <Menu.Trigger>Actions</Menu.Trigger>
43
- <Menu.Content>
44
- <Menu.Item onSelect={() => console.log('Edit')}>Edit</Menu.Item>
45
- <Menu.Item onSelect={() => console.log('Copy')}>Copy</Menu.Item>
46
- <Menu.Separator />
47
- <Menu.Item onSelect={() => console.log('Delete')}>Delete</Menu.Item>
48
- </Menu.Content>
49
- </Menu>
50
- );
51
- }
52
- ```
53
-
54
- ### Tabs
55
-
56
- ```tsx
57
- import { Tabs } from '@compa11y/react';
58
-
59
- function SettingsTabs() {
60
- return (
61
- <Tabs defaultValue="general">
62
- <Tabs.List>
63
- <Tabs.Tab value="general">General</Tabs.Tab>
64
- <Tabs.Tab value="security">Security</Tabs.Tab>
65
- <Tabs.Tab value="notifications">Notifications</Tabs.Tab>
66
- </Tabs.List>
67
- <Tabs.Panel value="general">General settings...</Tabs.Panel>
68
- <Tabs.Panel value="security">Security settings...</Tabs.Panel>
69
- <Tabs.Panel value="notifications">Notification settings...</Tabs.Panel>
70
- </Tabs>
71
- );
72
- }
73
- ```
74
-
75
- ### Toast
76
-
77
- ```tsx
78
- import { ToastProvider, ToastViewport, useToastHelpers } from '@compa11y/react';
79
-
80
- function App() {
81
- return (
82
- <ToastProvider>
83
- <Content />
84
- <ToastViewport position="bottom-right" />
85
- </ToastProvider>
86
- );
87
- }
88
-
89
- function Content() {
90
- const { success, error } = useToastHelpers();
91
-
92
- return <button onClick={() => success('Settings saved!')}>Save</button>;
93
- }
94
- ```
95
-
96
- ### Combobox
97
-
98
- ```tsx
99
- import { Combobox } from '@compa11y/react';
100
-
101
- const countries = [
102
- { value: 'us', label: 'United States' },
103
- { value: 'uk', label: 'United Kingdom' },
104
- { value: 'ca', label: 'Canada' },
105
- ];
106
-
107
- function CountrySelect() {
108
- const [value, setValue] = useState(null);
109
-
110
- return (
111
- <Combobox
112
- options={countries}
113
- value={value}
114
- onValueChange={setValue}
115
- aria-label="Select country"
116
- >
117
- <Combobox.Input placeholder="Choose a country..." clearable />
118
- <Combobox.Listbox emptyMessage="No countries found" />
119
- </Combobox>
120
- );
121
- }
122
- ```
123
-
124
- ### Select
125
-
126
- ```tsx
127
- import { Select } from '@compa11y/react';
128
-
129
- const fruits = [
130
- { value: 'apple', label: 'Apple' },
131
- { value: 'banana', label: 'Banana' },
132
- { value: 'cherry', label: 'Cherry' },
133
- { value: 'dragonfruit', label: 'Dragon Fruit', disabled: true },
134
- { value: 'elderberry', label: 'Elderberry' },
135
- ];
136
-
137
- function FruitPicker() {
138
- const [value, setValue] = useState(null);
139
-
140
- return (
141
- <Select
142
- options={fruits}
143
- value={value}
144
- onValueChange={setValue}
145
- aria-label="Choose a fruit"
146
- >
147
- <Select.Trigger placeholder="Pick a fruit..." />
148
- <Select.Listbox />
149
- </Select>
150
- );
151
- }
152
- ```
153
-
154
- **Keyboard Navigation:**
155
-
156
- | Key | Action |
157
- | ----------------- | ----------------------------------------- |
158
- | `Enter` / `Space` | Open listbox or select highlighted option |
159
- | `ArrowDown` | Open listbox / move highlight down |
160
- | `ArrowUp` | Open listbox / move highlight up |
161
- | `Home` / `End` | Jump to first / last option |
162
- | `Escape` | Close listbox |
163
- | `Tab` | Close listbox and move focus |
164
- | Type characters | Jump to matching option (type-ahead) |
165
-
166
- **Props:**
167
-
168
- | Prop | Type | Default | Description |
169
- | ----------------- | --------------------------------- | ----------------------- | ---------------------------- |
170
- | `options` | `SelectOption[]` | — | List of options |
171
- | `value` | `string \| null` | — | Controlled selected value |
172
- | `defaultValue` | `string` | — | Default value (uncontrolled) |
173
- | `onValueChange` | `(value: string \| null) => void` | — | Change handler |
174
- | `placeholder` | `string` | `'Select an option...'` | Trigger placeholder text |
175
- | `disabled` | `boolean` | `false` | Disable the select |
176
- | `aria-label` | `string` | — | Accessible label |
177
- | `aria-labelledby` | `string` | — | ID of labelling element |
178
-
179
- ### Switch
180
-
181
- ```tsx
182
- import { Switch } from '@compa11y/react';
183
-
184
- function NotificationSettings() {
185
- const [enabled, setEnabled] = useState(false);
186
-
187
- return (
188
- <Switch checked={enabled} onCheckedChange={setEnabled}>
189
- Enable notifications
190
- </Switch>
191
- );
192
- }
193
- ```
194
-
195
- **Customization:**
196
-
197
- ```css
198
- .my-switch {
199
- --compa11y-switch-bg: #d1d5db;
200
- --compa11y-switch-checked-bg: #10b981;
201
- --compa11y-switch-thumb-bg: white;
202
- --compa11y-switch-width: 3rem;
203
- --compa11y-switch-height: 1.75rem;
204
- --compa11y-focus-color: #10b981;
205
- }
206
- ```
207
-
208
- ### Listbox
209
-
210
- ```tsx
211
- import { Listbox } from '@compa11y/react';
212
-
213
- // Single select (selection follows focus)
214
- function FruitPicker() {
215
- const [fruit, setFruit] = useState('apple');
216
-
217
- return (
218
- <Listbox value={fruit} onValueChange={setFruit} aria-label="Favorite fruit">
219
- <Listbox.Group label="Citrus">
220
- <Listbox.Option value="orange">Orange</Listbox.Option>
221
- <Listbox.Option value="lemon">Lemon</Listbox.Option>
222
- <Listbox.Option value="grapefruit">Grapefruit</Listbox.Option>
223
- </Listbox.Group>
224
- <Listbox.Option value="apple">Apple</Listbox.Option>
225
- <Listbox.Option value="banana" disabled>
226
- Banana (sold out)
227
- </Listbox.Option>
228
- </Listbox>
229
- );
230
- }
231
-
232
- // Multi select (focus independent of selection)
233
- function ToppingsPicker() {
234
- const [toppings, setToppings] = useState(['cheese']);
235
-
236
- return (
237
- <Listbox
238
- multiple
239
- value={toppings}
240
- onValueChange={setToppings}
241
- aria-label="Pizza toppings"
242
- >
243
- <Listbox.Option value="cheese">Cheese</Listbox.Option>
244
- <Listbox.Option value="pepperoni">Pepperoni</Listbox.Option>
245
- <Listbox.Option value="mushrooms">Mushrooms</Listbox.Option>
246
- <Listbox.Option value="olives">Olives</Listbox.Option>
247
- </Listbox>
248
- );
249
- }
250
- ```
251
-
252
- **Props (Listbox):**
253
-
254
- | Prop | Type | Default | Description |
255
- | ----------------- | ------------------------------------- | ------------ | ----------------------------------------------------- |
256
- | `value` | `string \| string[]` | — | Controlled value (string for single, array for multi) |
257
- | `defaultValue` | `string \| string[]` | — | Default value (uncontrolled) |
258
- | `onValueChange` | `(value: string \| string[]) => void` | — | Change handler |
259
- | `multiple` | `boolean` | `false` | Enable multi-select mode |
260
- | `disabled` | `boolean` | `false` | Disable all options |
261
- | `discoverable` | `boolean` | `true` | Keep disabled listbox in tab order |
262
- | `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout orientation |
263
- | `unstyled` | `boolean` | `false` | Remove default styles |
264
- | `aria-label` | `string` | — | Accessible label |
265
- | `aria-labelledby` | `string` | — | ID of labelling element |
266
-
267
- **Props (Listbox.Option):**
268
-
269
- | Prop | Type | Default | Description |
270
- | -------------- | --------- | ------- | ------------------------------------------ |
271
- | `value` | `string` | — | Value for this option (required) |
272
- | `disabled` | `boolean` | `false` | Disable this option |
273
- | `discoverable` | `boolean` | `true` | Keep disabled option discoverable |
274
- | `unstyled` | `boolean` | — | Remove default styles (inherits from root) |
275
- | `aria-label` | `string` | — | Accessible label override |
276
-
277
- **Props (Listbox.Group):**
278
-
279
- | Prop | Type | Default | Description |
280
- | ---------- | --------- | ------- | ------------------------------------------ |
281
- | `label` | `string` | — | Group label (required, visible) |
282
- | `disabled` | `boolean` | `false` | Disable all options in group |
283
- | `unstyled` | `boolean` | — | Remove default styles (inherits from root) |
284
-
285
- **Keyboard Navigation:**
286
-
287
- | Key | Single Select | Multi Select |
288
- | ----------------------- | ---------------------------- | -------------------------- |
289
- | `ArrowDown` / `ArrowUp` | Move focus and select | Move focus only |
290
- | `Home` / `End` | First/last option and select | Move focus only |
291
- | `Space` | — | Toggle focused option |
292
- | `Shift+Arrow` | — | Move focus and toggle |
293
- | `Ctrl+Shift+Home/End` | — | Select range to first/last |
294
- | `Ctrl+A` | — | Toggle select all |
295
- | Type characters | Jump to match and select | Jump to match |
296
-
297
- **Customization:**
298
-
299
- ```css
300
- [data-compa11y-listbox] {
301
- --compa11y-listbox-bg: white;
302
- --compa11y-listbox-border: 1px solid #ccc;
303
- --compa11y-listbox-radius: 6px;
304
- --compa11y-listbox-max-height: 300px;
305
- }
306
-
307
- [data-compa11y-listbox-option] {
308
- --compa11y-option-hover-bg: #f5f5f5;
309
- --compa11y-option-focused-bg: #e6f0ff;
310
- --compa11y-option-selected-bg: #e6f0ff;
311
- --compa11y-option-selected-color: #10b981;
312
- --compa11y-option-check-color: #10b981;
313
- --compa11y-option-disabled-color: #999;
314
- --compa11y-focus-color: #10b981;
315
- }
316
- ```
317
-
318
- ### Input
319
-
320
- ```tsx
321
- import { Input } from '@compa11y/react';
322
-
323
- function ContactForm() {
324
- const [name, setName] = useState('');
325
- const [nameError, setNameError] = useState('');
326
-
327
- const validate = () => {
328
- if (!name.trim()) setNameError('Name is required');
329
- else setNameError('');
330
- };
331
-
332
- return (
333
- <Input
334
- label="Full Name"
335
- hint="Enter your first and last name"
336
- error={nameError || undefined}
337
- required
338
- placeholder="John Doe"
339
- value={name}
340
- onValueChange={setName}
341
- onBlur={validate}
342
- />
343
- );
344
- }
345
-
346
- // Compound mode for custom layouts
347
- function CustomInput() {
348
- const [value, setValue] = useState('');
349
-
350
- return (
351
- <Input value={value} onValueChange={setValue}>
352
- <Input.Label>Email</Input.Label>
353
- <Input.Field type="email" placeholder="you@example.com" />
354
- <Input.Hint>We'll never share your email</Input.Hint>
355
- <Input.Error>{/* error message here */}</Input.Error>
356
- </Input>
357
- );
358
- }
359
- ```
360
-
361
- **Props:**
362
-
363
- | Prop | Type | Default | Description |
364
- | ----------------- | ------------------------- | -------- | ------------------------------------------------------------ |
365
- | `label` | `ReactNode` | — | Visible label text |
366
- | `hint` | `ReactNode` | — | Hint/description text |
367
- | `error` | `ReactNode` | — | Error message (enables `aria-invalid`) |
368
- | `value` | `string` | — | Controlled value |
369
- | `defaultValue` | `string` | `''` | Default value (uncontrolled) |
370
- | `onValueChange` | `(value: string) => void` | — | Change handler |
371
- | `type` | `string` | `'text'` | Input type (text, email, password, number, tel, url, search) |
372
- | `placeholder` | `string` | — | Placeholder text |
373
- | `required` | `boolean` | `false` | Required field |
374
- | `disabled` | `boolean` | `false` | Disable the input |
375
- | `readOnly` | `boolean` | `false` | Read-only input |
376
- | `unstyled` | `boolean` | `false` | Remove default styles |
377
- | `aria-label` | `string` | — | Accessible label (when no visible label) |
378
- | `aria-labelledby` | `string` | — | ID of labelling element |
379
-
380
- **Customization:**
381
-
382
- ```css
383
- .my-input {
384
- --compa11y-input-border: 1px solid #ccc;
385
- --compa11y-input-border-focus: #10b981;
386
- --compa11y-input-border-error: #ef4444;
387
- --compa11y-input-radius: 8px;
388
- --compa11y-input-label-weight: 600;
389
- --compa11y-input-error-color: #ef4444;
390
- --compa11y-input-hint-color: #666;
391
- --compa11y-focus-color: #10b981;
392
- }
393
- ```
394
-
395
- ### Button
396
-
397
- ```tsx
398
- import { Button } from '@compa11y/react';
399
-
400
- function Actions() {
401
- return (
402
- <div style={{ display: 'flex', gap: '0.5rem' }}>
403
- <Button variant="primary" onClick={handleSave}>
404
- Save
405
- </Button>
406
- <Button variant="outline" onClick={handleCancel}>
407
- Cancel
408
- </Button>
409
- <Button variant="danger" onClick={handleDelete}>
410
- Delete
411
- </Button>
412
- </div>
413
- );
414
- }
415
-
416
- // Loading state
417
- function SaveButton() {
418
- const [loading, setLoading] = useState(false);
419
-
420
- const handleSave = async () => {
421
- setLoading(true);
422
- await saveData();
423
- setLoading(false);
424
- };
425
-
426
- return (
427
- <Button variant="primary" loading={loading} onClick={handleSave}>
428
- {loading ? 'Saving...' : 'Save Changes'}
429
- </Button>
430
- );
431
- }
432
-
433
- // Disabled but discoverable (stays in tab order)
434
- <Button variant="primary" disabled discoverable>
435
- Unavailable
436
- </Button>;
437
- ```
438
-
439
- **Props:**
440
-
441
- | Prop | Type | Default | Description |
442
- | -------------- | -------------------------------------------------------------- | ------------- | ------------------------------------------------------ |
443
- | `variant` | `'primary' \| 'secondary' \| 'danger' \| 'outline' \| 'ghost'` | `'secondary'` | Visual variant |
444
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
445
- | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
446
- | `disabled` | `boolean` | `false` | Disable the button |
447
- | `discoverable` | `boolean` | `false` | Keep disabled button in tab order with `aria-disabled` |
448
- | `loading` | `boolean` | `false` | Loading state (shows spinner, sets `aria-busy`) |
449
- | `unstyled` | `boolean` | `false` | Remove default styles |
450
- | `aria-label` | `string` | — | Accessible label |
451
-
452
- **Customization:**
453
-
454
- ```css
455
- [data-compa11y-button] {
456
- --compa11y-button-radius: 8px;
457
- --compa11y-button-font-weight: 600;
458
- --compa11y-button-primary-bg: #10b981;
459
- --compa11y-button-primary-color: white;
460
- --compa11y-button-danger-bg: #ef4444;
461
- --compa11y-button-danger-color: white;
462
- --compa11y-button-disabled-opacity: 0.5;
463
- --compa11y-focus-color: #10b981;
464
- }
465
- ```
466
-
467
- ### Textarea
468
-
469
- ```tsx
470
- import { Textarea } from '@compa11y/react';
471
-
472
- function FeedbackForm() {
473
- const [desc, setDesc] = useState('');
474
- const [descError, setDescError] = useState('');
475
-
476
- const validate = () => {
477
- if (!desc.trim()) setDescError('Description is required');
478
- else if (desc.trim().length < 10)
479
- setDescError('Must be at least 10 characters');
480
- else setDescError('');
481
- };
482
-
483
- return (
484
- <Textarea
485
- label="Description"
486
- hint="Provide at least 10 characters"
487
- error={descError || undefined}
488
- required
489
- rows={4}
490
- placeholder="Enter a description..."
491
- value={desc}
492
- onValueChange={setDesc}
493
- onBlur={validate}
494
- />
495
- );
496
- }
497
-
498
- // Compound mode for custom layouts
499
- function CustomTextarea() {
500
- const [value, setValue] = useState('');
501
-
502
- return (
503
- <Textarea value={value} onValueChange={setValue}>
504
- <Textarea.Label>Bio</Textarea.Label>
505
- <Textarea.Field rows={5} placeholder="Tell us about yourself..." />
506
- <Textarea.Hint>Markdown is supported</Textarea.Hint>
507
- <Textarea.Error>{/* error message here */}</Textarea.Error>
508
- </Textarea>
509
- );
510
- }
511
- ```
512
-
513
- **Props:**
514
-
515
- | Prop | Type | Default | Description |
516
- | ----------------- | ------------------------- | ------------ | -------------------------------------------------- |
517
- | `label` | `ReactNode` | — | Visible label text |
518
- | `hint` | `ReactNode` | — | Hint/description text |
519
- | `error` | `ReactNode` | — | Error message (enables `aria-invalid`) |
520
- | `value` | `string` | — | Controlled value |
521
- | `defaultValue` | `string` | `''` | Default value (uncontrolled) |
522
- | `onValueChange` | `(value: string) => void` | — | Change handler |
523
- | `rows` | `number` | `3` | Number of visible text rows |
524
- | `resize` | `string` | `'vertical'` | Resize behavior (none, both, horizontal, vertical) |
525
- | `placeholder` | `string` | — | Placeholder text |
526
- | `required` | `boolean` | `false` | Required field |
527
- | `disabled` | `boolean` | `false` | Disable the textarea |
528
- | `readOnly` | `boolean` | `false` | Read-only textarea |
529
- | `unstyled` | `boolean` | `false` | Remove default styles |
530
- | `aria-label` | `string` | — | Accessible label (when no visible label) |
531
- | `aria-labelledby` | `string` | — | ID of labelling element |
532
-
533
- **Customization:**
534
-
535
- ```css
536
- .my-textarea {
537
- --compa11y-textarea-border: 1px solid #ccc;
538
- --compa11y-textarea-border-focus: #10b981;
539
- --compa11y-textarea-border-error: #ef4444;
540
- --compa11y-textarea-radius: 8px;
541
- --compa11y-textarea-label-weight: 600;
542
- --compa11y-textarea-error-color: #ef4444;
543
- --compa11y-textarea-hint-color: #666;
544
- --compa11y-focus-color: #10b981;
545
- }
546
- ```
13
+ Dialog, ActionMenu, Tabs, Toast, Combobox, Select, Listbox, Checkbox, RadioGroup, Switch, Input, Textarea, Button
547
14
 
548
15
  ## Hooks
549
16
 
550
- ### useFocusTrap
551
-
552
- ```tsx
553
- import { useFocusTrap } from '@compa11y/react';
554
-
555
- function Modal({ isOpen }) {
556
- const trapRef = useFocusTrap({ active: isOpen });
557
-
558
- return (
559
- <div ref={trapRef} role="dialog">
560
- <button>Close</button>
561
- </div>
562
- );
563
- }
564
- ```
565
-
566
- ### useAnnouncer
567
-
568
- ```tsx
569
- import { useAnnouncer } from '@compa11y/react';
570
-
571
- function SearchResults({ count }) {
572
- const { announce } = useAnnouncer();
17
+ `useFocusTrap`, `useFocusVisible`, `useFocusNeighbor`, `useFocusReturn`, `useKeyboard`, `useMenuKeyboard`, `useTabsKeyboard`, `useGridKeyboard`, `useTypeAhead`, `useAnnouncer`, `useRovingTabindex`, and more.
573
18
 
574
- useEffect(() => {
575
- announce(`Found ${count} results`);
576
- }, [count, announce]);
577
- }
578
- ```
579
-
580
- ### useKeyboard
19
+ ## Quick start
581
20
 
582
21
  ```tsx
583
- import { useKeyboard } from '@compa11y/react';
584
-
585
- function CustomList() {
586
- const keyboardProps = useKeyboard({
587
- ArrowDown: () => focusNext(),
588
- ArrowUp: () => focusPrevious(),
589
- Enter: () => selectItem(),
590
- });
22
+ import { Dialog, Select, useToastHelpers } from '@compa11y/react';
591
23
 
592
- return <ul {...keyboardProps}>...</ul>;
593
- }
24
+ // Fully accessible dialog — focus trap, Escape to close, screen reader announcements
25
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
26
+ <Dialog.Trigger>Open</Dialog.Trigger>
27
+ <Dialog.Title>Confirm</Dialog.Title>
28
+ <Dialog.Content>Are you sure?</Dialog.Content>
29
+ <Dialog.Actions>
30
+ <button onClick={() => setIsOpen(false)}>Cancel</button>
31
+ <button onClick={handleConfirm}>Confirm</button>
32
+ </Dialog.Actions>
33
+ </Dialog>
594
34
  ```
595
35
 
596
- ### useFocusVisible
597
-
598
- ```tsx
599
- import { useFocusVisible } from '@compa11y/react';
600
-
601
- function Button({ children }) {
602
- const { isFocusVisible, focusProps } = useFocusVisible();
603
-
604
- return (
605
- <button {...focusProps} className={isFocusVisible ? 'focus-ring' : ''}>
606
- {children}
607
- </button>
608
- );
609
- }
610
- ```
611
-
612
- ### useRovingTabindex
613
-
614
- ```tsx
615
- import { useRovingTabindex } from '@compa11y/react';
616
-
617
- function Toolbar() {
618
- const { getItemProps } = useRovingTabindex({
619
- itemCount: 3,
620
- orientation: 'horizontal',
621
- });
36
+ ## Documentation
622
37
 
623
- return (
624
- <div role="toolbar">
625
- <button {...getItemProps(0)}>Cut</button>
626
- <button {...getItemProps(1)}>Copy</button>
627
- <button {...getItemProps(2)}>Paste</button>
628
- </div>
629
- );
630
- }
631
- ```
632
-
633
- ## Styling
634
-
635
- All components are unstyled. Use `data-*` attributes for state-based styling:
636
-
637
- ```css
638
- /* Dialog */
639
- [data-compa11y-dialog-overlay] {
640
- background: rgba(0, 0, 0, 0.5);
641
- }
642
-
643
- [data-compa11y-dialog] {
644
- background: white;
645
- padding: 1.5rem;
646
- border-radius: 8px;
647
- }
648
-
649
- /* Menu */
650
- [data-compa11y-menu-content] {
651
- background: white;
652
- border: 1px solid #e0e0e0;
653
- }
654
-
655
- [data-compa11y-menu-item][data-highlighted='true'] {
656
- background: #f0f0f0;
657
- }
658
-
659
- /* Tabs */
660
- [data-compa11y-tab][data-selected='true'] {
661
- border-bottom: 2px solid blue;
662
- }
663
-
664
- /* Combobox */
665
- [data-compa11y-combobox-option][data-highlighted='true'] {
666
- background: #f0f0f0;
667
- }
668
-
669
- /* Listbox */
670
- [data-compa11y-listbox] {
671
- border: 1px solid #e0e0e0;
672
- border-radius: 6px;
673
- max-height: 300px;
674
- overflow-y: auto;
675
- }
676
-
677
- [data-compa11y-listbox-option][data-focused='true'] {
678
- background: #e6f0ff;
679
- }
680
-
681
- [data-compa11y-listbox-option][data-selected='true'] {
682
- background: #e6f0ff;
683
- font-weight: 600;
684
- }
685
-
686
- /* Select */
687
- [data-compa11y-select] {
688
- position: relative;
689
- width: 300px;
690
- }
691
-
692
- [data-compa11y-select-trigger] {
693
- width: 100%;
694
- display: flex;
695
- align-items: center;
696
- justify-content: space-between;
697
- padding: 0.5rem 2rem 0.5rem 0.75rem;
698
- border: 1px solid #ccc;
699
- border-radius: 4px;
700
- background: white;
701
- cursor: pointer;
702
- text-align: left;
703
- }
704
-
705
- [data-compa11y-select-listbox] {
706
- position: absolute;
707
- top: 100%;
708
- left: 0;
709
- right: 0;
710
- margin-top: 4px;
711
- background: white;
712
- border: 1px solid #e0e0e0;
713
- border-radius: 4px;
714
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
715
- max-height: 200px;
716
- overflow-y: auto;
717
- z-index: 1000;
718
- list-style: none;
719
- padding: 0;
720
- }
721
-
722
- [data-compa11y-select-option][data-highlighted='true'] {
723
- background: #f0f0f0;
724
- }
725
-
726
- [data-compa11y-select-option][data-selected='true'] {
727
- background: #e6f0ff;
728
- font-weight: 600;
729
- }
730
- ```
38
+ Full documentation, live examples, props reference, and accessibility details at **[compa11y.org](https://compa11y.org)**.
731
39
 
732
40
  ## License
733
41