@expressivecss/mcp-server 0.3.0 → 0.3.1
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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/capability-roadmap.json +1908 -221
- package/component-decisions.json +4406 -667
- package/component-guides.json +158 -42
- package/contract.json +6 -4
- package/package.json +2 -1
- package/semantics-data.json +601 -3
- package/server.js +4 -2
- package/smoke.mjs +18 -1
package/semantics-data.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedBy": "mcp/expressivecss/scripts/sync-guides.mjs",
|
|
4
|
-
"frameworkVersion": "0.
|
|
4
|
+
"frameworkVersion": "0.13.1",
|
|
5
5
|
"semantics": {
|
|
6
6
|
"about": "Normative HTML semantics for ExpressiveCSS. This data file is the single source of truth: SEMANTICS.md is generated from it by scripts/gen-semantics.mjs, and tests/semantics.test.js enforces it. One row per roster entry, each stating its kind - component, foundation or behavior, as CONTEXT.md defines them. CONTEXT.md defines the rest of the vocabulary.",
|
|
7
7
|
"scope": "Element choice, landmark structure, and the ARIA that element choice implies. Keyboard interaction, focus management and colour contrast are deliberately out of scope.",
|
|
@@ -350,6 +350,19 @@
|
|
|
350
350
|
}
|
|
351
351
|
]
|
|
352
352
|
},
|
|
353
|
+
"aura": {
|
|
354
|
+
"status": "enforced",
|
|
355
|
+
"note": "Added with the aura component. An aura wraps one element and draws its light with a pseudo-element.",
|
|
356
|
+
"rules": [
|
|
357
|
+
{
|
|
358
|
+
"id": "aura-wraps-one-element",
|
|
359
|
+
"kind": "forbid",
|
|
360
|
+
"selector": ".aura > * + *",
|
|
361
|
+
"fragmentSafe": true,
|
|
362
|
+
"message": "An .aura wraps one element. Give each highlighted element its own .aura, and keep to one per screen."
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
},
|
|
353
366
|
"carousel": {
|
|
354
367
|
"status": "enforced",
|
|
355
368
|
"note": "Swept 0.8.0. Material 3 items remain exposed because several can be visible at once. Checked at runtime in tests/generated-a11y.test.js. Its keyboard contract is implemented and tested, so tablist is rejected rather than withheld. The role rule keeps out every composite role but not every role: role=\"group\" with aria-roledescription=\"carousel\" is the pattern carousel.ts writes and stays legal.",
|
|
@@ -564,6 +577,26 @@
|
|
|
564
577
|
}
|
|
565
578
|
]
|
|
566
579
|
},
|
|
580
|
+
"forms/drop-zone": {
|
|
581
|
+
"status": "enforced",
|
|
582
|
+
"note": "Added with the drop zone component. A <label> wraps the native file input, which covers it, so the label text names the input and the browser handles clicks, keys and dropped files.",
|
|
583
|
+
"rules": [
|
|
584
|
+
{
|
|
585
|
+
"id": "drop-zone-input-is-in-the-label",
|
|
586
|
+
"kind": "forbid",
|
|
587
|
+
"selector": ".drop-zone > input[type=\"file\"]",
|
|
588
|
+
"fragmentSafe": true,
|
|
589
|
+
"message": "Put the file input inside the .drop-zone <label>. The label names the input and the input covers the label; a loose input is neither named nor a drop target."
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
"id": "drop-zone-files-are-a-list",
|
|
593
|
+
"kind": "forbid",
|
|
594
|
+
"selector": ".drop-zone-files:not(ul, ol)",
|
|
595
|
+
"fragmentSafe": true,
|
|
596
|
+
"message": "Write .drop-zone-files as a <ul>. The behavior fills it with one <li> per file, and a list tells assistive technology how many files were chosen."
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
},
|
|
567
600
|
"forms/fieldset": {
|
|
568
601
|
"status": "enforced",
|
|
569
602
|
"note": "Swept 0.8.0.",
|
|
@@ -597,7 +630,7 @@
|
|
|
597
630
|
},
|
|
598
631
|
"forms/input-fields": {
|
|
599
632
|
"status": "enforced",
|
|
600
|
-
"note": "Swept 0.8.0. .field is the container; icons name their side and are hidden; supporting text is linked to the control it describes.",
|
|
633
|
+
"note": "Swept 0.8.0. .field is the container; icons name their side and are hidden; supporting text is linked to the control it describes. A .password-toggle reports its state with aria-pressed; input.otp is one native input drawn as cells.",
|
|
601
634
|
"rules": [
|
|
602
635
|
{
|
|
603
636
|
"id": "field-container-class",
|
|
@@ -653,6 +686,28 @@
|
|
|
653
686
|
"kind": "forbid",
|
|
654
687
|
"selector": ".field:has(> small) > :is(input, textarea, select):not([aria-describedby])",
|
|
655
688
|
"message": "A field with supporting text must point at it with aria-describedby, or the text is never read out with the control."
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"id": "password-toggle-reports-its-state",
|
|
692
|
+
"kind": "require-attr",
|
|
693
|
+
"selector": ".password-toggle",
|
|
694
|
+
"attr": "aria-pressed",
|
|
695
|
+
"message": "Give a .password-toggle aria-pressed=\"false\". The behavior flips it, and its name stays \"Show password\", so the pressed state is how a screen reader user hears that the password is visible."
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"id": "otp-autocompletes",
|
|
699
|
+
"kind": "require-attr",
|
|
700
|
+
"selector": "input.otp",
|
|
701
|
+
"attr": "autocomplete",
|
|
702
|
+
"equals": "one-time-code",
|
|
703
|
+
"message": "Set autocomplete=\"one-time-code\" on a one-time code input so the browser can offer the code from a message."
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"id": "otp-is-not-a-number",
|
|
707
|
+
"kind": "forbid",
|
|
708
|
+
"selector": "input.otp[type=\"number\"]",
|
|
709
|
+
"fragmentSafe": true,
|
|
710
|
+
"message": "A code is not a quantity: type=\"number\" drops leading zeros and adds a spinner. Use type=\"text\" with inputmode=\"numeric\"."
|
|
656
711
|
}
|
|
657
712
|
]
|
|
658
713
|
},
|
|
@@ -674,6 +729,32 @@
|
|
|
674
729
|
}
|
|
675
730
|
]
|
|
676
731
|
},
|
|
732
|
+
"forms/rating": {
|
|
733
|
+
"status": "enforced",
|
|
734
|
+
"note": "Added with the rating component. The input is a fieldset of native radios drawn as stars; the display is a named image.",
|
|
735
|
+
"rules": [
|
|
736
|
+
{
|
|
737
|
+
"id": "rating-is-a-fieldset-or-an-image",
|
|
738
|
+
"kind": "forbid",
|
|
739
|
+
"selector": ".rating:not(fieldset):not([role=\"img\"])",
|
|
740
|
+
"fragmentSafe": true,
|
|
741
|
+
"message": "A .rating is either a <fieldset> of radios, for input, or an element with role=\"img\" and a name, for display. Anything else draws stars that say nothing."
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"id": "rating-display-is-named",
|
|
745
|
+
"kind": "require-accessible-name",
|
|
746
|
+
"selector": ".rating[role=\"img\"]",
|
|
747
|
+
"message": "Name a rating display with the value, such as aria-label=\"Rated 4.5 out of 5\"."
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"id": "rating-label-text-precedes-the-radio",
|
|
751
|
+
"kind": "forbid",
|
|
752
|
+
"selector": "fieldset.rating [type=\"radio\"] + span",
|
|
753
|
+
"fragmentSafe": true,
|
|
754
|
+
"message": "Put a star's visually hidden text before its radio. A span after the radio is the older input + span pattern, which hides the radio and with it the star."
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
},
|
|
677
758
|
"forms/select": {
|
|
678
759
|
"status": "enforced",
|
|
679
760
|
"note": "Swept 0.8.0. The listbox FormSelect builds is checked at runtime in tests/select.test.js.",
|
|
@@ -840,7 +921,7 @@
|
|
|
840
921
|
},
|
|
841
922
|
"menu": {
|
|
842
923
|
"status": "enforced",
|
|
843
|
-
"note": "Swept 0.8.0.",
|
|
924
|
+
"note": "Swept 0.8.0. A [data-context-menu] trigger is a region: the menu opens at the pointer and the region carries no expanded state.",
|
|
844
925
|
"rules": [
|
|
845
926
|
{
|
|
846
927
|
"id": "menu-children-are-list-items",
|
|
@@ -848,6 +929,523 @@
|
|
|
848
929
|
"selector": "menu > :not(li):not(script):not(template)",
|
|
849
930
|
"fragmentSafe": true,
|
|
850
931
|
"message": "<menu> is a list: its content model permits only <li>. A bare <hr> between entries is invalid - put the separator inside an <li>, or use role=separator there."
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"id": "context-menu-region-is-not-a-control",
|
|
935
|
+
"kind": "forbid",
|
|
936
|
+
"selector": "[data-context-menu]:is(button, a[href], input, select, textarea)",
|
|
937
|
+
"fragmentSafe": true,
|
|
938
|
+
"message": "data-context-menu goes on a region of content. A button or link already has an action for Enter and click; give it its own menu trigger instead."
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
"message": {
|
|
943
|
+
"status": "enforced",
|
|
944
|
+
"note": "Added with the message component. The parts are divs on a CSS grid. A <header> or <footer> outside sectioning content is a banner or contentinfo landmark, so writing the parts as those elements would add two landmarks per message. Icon-only footer actions are already covered by icon-only-control-is-named.",
|
|
945
|
+
"rules": [
|
|
946
|
+
{
|
|
947
|
+
"id": "message-parts-are-not-landmarks",
|
|
948
|
+
"kind": "forbid",
|
|
949
|
+
"selector": ":is(header.message-header, footer.message-footer)",
|
|
950
|
+
"fragmentSafe": true,
|
|
951
|
+
"message": "Write .message-header and .message-footer as <div>. Outside sectioning content a <header> is a banner landmark and a <footer> is contentinfo, so a conversation would fill the landmark menu with one of each per message."
|
|
952
|
+
}
|
|
953
|
+
]
|
|
954
|
+
},
|
|
955
|
+
"message-scroller": {
|
|
956
|
+
"status": "enforced",
|
|
957
|
+
"note": "Added with the message scroller component. The viewport is a focusable scroll container, so it needs a name; on a live conversation it is also the log. The script owns scroll position and the button's inert state, and adds no roles.",
|
|
958
|
+
"rules": [
|
|
959
|
+
{
|
|
960
|
+
"id": "message-scroller-viewport-is-named",
|
|
961
|
+
"kind": "require-accessible-name",
|
|
962
|
+
"selector": ".message-scroller-viewport",
|
|
963
|
+
"message": "Name the viewport with aria-label. It is a focusable scroll region, and a screen reader announces the name when focus lands on it."
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
},
|
|
967
|
+
"rich-text-editor": {
|
|
968
|
+
"status": "enforced",
|
|
969
|
+
"note": "Added with the rich text editor component. The script hides the textarea and gives the editable element role=\"textbox\" with aria-multiline, copying the textarea's name to it, so the name has to be on the textarea. The toolbar follows the toolbar rules; icon-only buttons are covered by icon-only-control-is-named.",
|
|
970
|
+
"rules": [
|
|
971
|
+
{
|
|
972
|
+
"id": "rich-text-editor-textarea-is-named",
|
|
973
|
+
"kind": "forbid",
|
|
974
|
+
"selector": ".rich-text-editor > textarea:not([aria-label], [aria-labelledby], [id])",
|
|
975
|
+
"fragmentSafe": true,
|
|
976
|
+
"message": "Name the textarea with a <label for>, aria-label or aria-labelledby. The editor copies that name to its editable area, which has none of its own. Its content is the starting HTML, not a name."
|
|
977
|
+
}
|
|
978
|
+
]
|
|
979
|
+
},
|
|
980
|
+
"accordion": {
|
|
981
|
+
"status": "enforced",
|
|
982
|
+
"note": "Added with the accordion component. Each item is a native <details>, so the browser exposes <summary> as a button with its expanded state and handles the keyboard. The rules keep authors from re-stating that state or nesting controls in the toggle.",
|
|
983
|
+
"rules": [
|
|
984
|
+
{
|
|
985
|
+
"id": "accordion-items-are-details",
|
|
986
|
+
"kind": "forbid",
|
|
987
|
+
"selector": ".accordion > :not(details)",
|
|
988
|
+
"fragmentSafe": true,
|
|
989
|
+
"message": "Write every accordion item as <details> with a <summary>. The element is the disclosure: a div with a click handler has no expanded state and no keyboard support."
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
"id": "accordion-summary-holds-no-controls",
|
|
993
|
+
"kind": "forbid",
|
|
994
|
+
"selector": ".accordion summary :is(a[href], button, input, select, textarea, [tabindex])",
|
|
995
|
+
"fragmentSafe": true,
|
|
996
|
+
"message": "A <summary> is already the toggle button. A link or control inside it is a control nested in a control: pressing it also opens or closes the item, and screen readers flatten it into the summary name. Put actions in the panel."
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
"id": "accordion-summary-state-is-native",
|
|
1000
|
+
"kind": "forbid",
|
|
1001
|
+
"selector": ".accordion summary:is([role=\"button\"], [aria-expanded])",
|
|
1002
|
+
"fragmentSafe": true,
|
|
1003
|
+
"message": "The browser exposes <summary> as a button and reports its expanded state from the open attribute. An authored role=\"button\" repeats it, and an authored aria-expanded goes stale the first time the item toggles."
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
},
|
|
1007
|
+
"data-table": {
|
|
1008
|
+
"status": "enforced",
|
|
1009
|
+
"note": "Added with the data table component. The wrapper is the scroll container so the <table> keeps its table semantics. Sorting and selection are page state: aria-sort on the header cell and a checkbox in the first cell.",
|
|
1010
|
+
"rules": [
|
|
1011
|
+
{
|
|
1012
|
+
"id": "data-table-wraps-the-table",
|
|
1013
|
+
"kind": "forbid",
|
|
1014
|
+
"selector": "table.data-table",
|
|
1015
|
+
"fragmentSafe": true,
|
|
1016
|
+
"message": "Put .data-table on a <div> around the <table>. The wrapper scrolls; a table made to scroll itself needs display:block, which drops its table role in some engines."
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"id": "data-table-sort-is-on-a-header",
|
|
1020
|
+
"kind": "forbid",
|
|
1021
|
+
"selector": ".data-table [aria-sort]:not(th)",
|
|
1022
|
+
"fragmentSafe": true,
|
|
1023
|
+
"message": "aria-sort belongs on the column header cell (<th>), not on the button inside it or on a data cell. Assistive technology reads the sort state from the header."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"id": "data-table-row-is-not-selected",
|
|
1027
|
+
"kind": "forbid",
|
|
1028
|
+
"selector": ".data-table tr[aria-selected]",
|
|
1029
|
+
"fragmentSafe": true,
|
|
1030
|
+
"message": "aria-selected is not valid on a row of a plain table. It belongs to role=\"grid\" and its keyboard contract. Select a row with a labelled checkbox in its first cell."
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
"id": "data-table-scroll-region-is-a-region",
|
|
1034
|
+
"kind": "require-attr",
|
|
1035
|
+
"selector": ".data-table[tabindex]",
|
|
1036
|
+
"attr": "role",
|
|
1037
|
+
"equals": "region",
|
|
1038
|
+
"message": "A focusable scroll wrapper needs role=\"region\" so its name is announced. aria-label is not allowed on a plain div."
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"id": "data-table-scroll-region-is-named",
|
|
1042
|
+
"kind": "require-accessible-name",
|
|
1043
|
+
"selector": ".data-table[tabindex]",
|
|
1044
|
+
"message": "Name a focusable scroll wrapper with aria-label, or aria-labelledby pointing at the table caption, so keyboard users know what they are scrolling."
|
|
1045
|
+
}
|
|
1046
|
+
]
|
|
1047
|
+
},
|
|
1048
|
+
"avatar": {
|
|
1049
|
+
"status": "enforced",
|
|
1050
|
+
"note": "Added with the avatar component. Initials and icon ligatures read as letters, so a non-image avatar is either a named role=\"img\" or hidden beside a visible name.",
|
|
1051
|
+
"rules": [
|
|
1052
|
+
{
|
|
1053
|
+
"id": "avatar-image-has-alt",
|
|
1054
|
+
"kind": "forbid",
|
|
1055
|
+
"selector": "img.avatar:not([alt])",
|
|
1056
|
+
"message": "Give an avatar image alt text naming the person, or alt=\"\" when their name is already beside it."
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
"id": "avatar-is-an-image-or-hidden",
|
|
1060
|
+
"kind": "forbid",
|
|
1061
|
+
"selector": ".avatar:not(img):not([role=\"img\"]):not([aria-hidden=\"true\"])",
|
|
1062
|
+
"fragmentSafe": true,
|
|
1063
|
+
"message": "Initials read as separate letters and an icon reads as its ligature name. Give a non-image avatar role=\"img\" and a name, or aria-hidden=\"true\" when the person is named next to it."
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"id": "avatar-image-role-is-named",
|
|
1067
|
+
"kind": "require-accessible-name",
|
|
1068
|
+
"selector": ".avatar[role=\"img\"]",
|
|
1069
|
+
"message": "An avatar with role=\"img\" needs aria-label or aria-labelledby. Its initials are hidden from the name computation of an image."
|
|
1070
|
+
}
|
|
1071
|
+
]
|
|
1072
|
+
},
|
|
1073
|
+
"empty-state": {
|
|
1074
|
+
"status": "enforced",
|
|
1075
|
+
"note": "Added with the empty state component. Layout around authored content: the heading, text and buttons carry their own semantics, and the icon rules cover a leading icon.",
|
|
1076
|
+
"rules": [
|
|
1077
|
+
{
|
|
1078
|
+
"id": "empty-state-image-has-alt",
|
|
1079
|
+
"kind": "forbid",
|
|
1080
|
+
"selector": ".empty-state > img:not([alt])",
|
|
1081
|
+
"message": "Give an empty-state illustration alt=\"\" when the heading already says what it shows, or alt text when the image adds information."
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
},
|
|
1085
|
+
"countdown": {
|
|
1086
|
+
"status": "enforced",
|
|
1087
|
+
"note": "Added with the countdown component. The digits are generated content hidden from assistive technology, so the element's text is the value a screen reader reads.",
|
|
1088
|
+
"rules": [
|
|
1089
|
+
{
|
|
1090
|
+
"id": "countdown-has-text",
|
|
1091
|
+
"kind": "forbid",
|
|
1092
|
+
"selector": ".countdown:empty",
|
|
1093
|
+
"fragmentSafe": true,
|
|
1094
|
+
"message": "Write the number as the countdown's text, matching --md-comp-countdown-value. The rolling digits are hidden from screen readers, so an empty countdown reads as nothing."
|
|
1095
|
+
}
|
|
1096
|
+
]
|
|
1097
|
+
},
|
|
1098
|
+
"popover": {
|
|
1099
|
+
"status": "enforced",
|
|
1100
|
+
"note": "Added with the popover component. The panel is a native [popover] and its button names it with popovertarget or commandfor, so the browser owns opening, light dismiss, Escape and the button's expanded state.",
|
|
1101
|
+
"rules": [
|
|
1102
|
+
{
|
|
1103
|
+
"id": "popover-is-native",
|
|
1104
|
+
"kind": "forbid",
|
|
1105
|
+
"selector": ".popover:not([popover])",
|
|
1106
|
+
"fragmentSafe": true,
|
|
1107
|
+
"message": "Give a .popover the popover attribute. Without it the panel is never in the top layer, never light-dismisses and its button reports no expanded state."
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
"id": "popover-target-exists",
|
|
1111
|
+
"kind": "require-idref",
|
|
1112
|
+
"selector": "[popovertarget]",
|
|
1113
|
+
"attr": "popovertarget",
|
|
1114
|
+
"container": "body",
|
|
1115
|
+
"targetSelector": "[popover]",
|
|
1116
|
+
"message": "popovertarget must name the id of a [popover] element in the page. A dangling id leaves a button that does nothing."
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"id": "popover-is-not-a-tooltip",
|
|
1120
|
+
"kind": "forbid",
|
|
1121
|
+
"selector": ".popover[role=\"tooltip\"]",
|
|
1122
|
+
"fragmentSafe": true,
|
|
1123
|
+
"message": "A popover opens on press and can hold links and buttons, which a tooltip never can. Leave the role off; the browser relates the panel to its button."
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
},
|
|
1127
|
+
"stepper": {
|
|
1128
|
+
"status": "enforced",
|
|
1129
|
+
"note": "Added with the stepper component. The ordered list carries the step count and position; aria-current=\"step\" marks the current step, and CSS alt text speaks the complete and invalid states.",
|
|
1130
|
+
"rules": [
|
|
1131
|
+
{
|
|
1132
|
+
"id": "stepper-is-an-ordered-list",
|
|
1133
|
+
"kind": "forbid",
|
|
1134
|
+
"selector": ".stepper:not(ol)",
|
|
1135
|
+
"fragmentSafe": true,
|
|
1136
|
+
"message": "Write a stepper as <ol class=\"stepper\">. The order is the content: an ordered list announces how many steps there are and which one each is."
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
"id": "stepper-current-is-a-step",
|
|
1140
|
+
"kind": "forbid",
|
|
1141
|
+
"selector": ".stepper > li[aria-current]:not([aria-current=\"step\"])",
|
|
1142
|
+
"fragmentSafe": true,
|
|
1143
|
+
"message": "Mark the current step with aria-current=\"step\". \"page\" or \"true\" tells assistive technology something else."
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
"id": "stepper-has-one-current-step",
|
|
1147
|
+
"kind": "forbid",
|
|
1148
|
+
"selector": ".stepper > li[aria-current=\"step\"] ~ li[aria-current=\"step\"]",
|
|
1149
|
+
"fragmentSafe": true,
|
|
1150
|
+
"message": "Only one step is current. Mark finished steps with .complete instead."
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
"id": "stepper-step-is-not-error-colored",
|
|
1154
|
+
"kind": "forbid",
|
|
1155
|
+
"selector": ".stepper > li.error",
|
|
1156
|
+
"fragmentSafe": true,
|
|
1157
|
+
"message": "Mark a step that needs attention with .invalid. .error is the error color utility: it paints the whole step red and is not spoken."
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
},
|
|
1161
|
+
"timeline": {
|
|
1162
|
+
"status": "enforced",
|
|
1163
|
+
"note": "Added with the timeline component. An ordered list of events; the order is the content.",
|
|
1164
|
+
"rules": [
|
|
1165
|
+
{
|
|
1166
|
+
"id": "timeline-is-an-ordered-list",
|
|
1167
|
+
"kind": "forbid",
|
|
1168
|
+
"selector": ".timeline:not(ol)",
|
|
1169
|
+
"fragmentSafe": true,
|
|
1170
|
+
"message": "Write a timeline as <ol class=\"timeline\">. The events have an order, and an ordered list says so to assistive technology."
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"id": "timeline-time-is-machine-readable",
|
|
1174
|
+
"kind": "require-attr",
|
|
1175
|
+
"selector": ".timeline time",
|
|
1176
|
+
"attr": "datetime",
|
|
1177
|
+
"message": "Give each <time> a datetime value. \"9:12 AM\" or \"Yesterday\" has no date a script, a translation tool or a reader in another time zone can use."
|
|
1178
|
+
}
|
|
1179
|
+
]
|
|
1180
|
+
},
|
|
1181
|
+
"stat": {
|
|
1182
|
+
"status": "enforced",
|
|
1183
|
+
"note": "Added with the stat component. A description list pairs each value with its label; the change arrow is decoration and the text says the direction.",
|
|
1184
|
+
"rules": [
|
|
1185
|
+
{
|
|
1186
|
+
"id": "stats-are-a-description-list",
|
|
1187
|
+
"kind": "forbid",
|
|
1188
|
+
"selector": ".stats:not(dl)",
|
|
1189
|
+
"fragmentSafe": true,
|
|
1190
|
+
"message": "Write .stats as a <dl> with a <dt> label and <dd> value per tile. Without the pairing a screen reader reads a run of numbers with nothing to say what each one is."
|
|
1191
|
+
}
|
|
1192
|
+
]
|
|
1193
|
+
},
|
|
1194
|
+
"kpi": {
|
|
1195
|
+
"status": "enforced",
|
|
1196
|
+
"note": "Added with the KPI component. A description list pairs the value with its label; the trend arrow is decoration and the text says the direction.",
|
|
1197
|
+
"rules": [
|
|
1198
|
+
{
|
|
1199
|
+
"id": "kpi-is-a-card-with-a-labelled-value",
|
|
1200
|
+
"kind": "forbid",
|
|
1201
|
+
"selector": ".kpi:not(article:has(> dl > dt + dd.kpi-value))",
|
|
1202
|
+
"message": "Write .kpi as an <article> holding a <dl> whose <dt> label comes right before the <dd class=\"kpi-value\">. The article makes it a card, and without the pairing a screen reader reads a bare number with nothing to say what it is. Not fragmentSafe: what it detects is an omission."
|
|
1203
|
+
}
|
|
1204
|
+
]
|
|
1205
|
+
},
|
|
1206
|
+
"marquee": {
|
|
1207
|
+
"status": "enforced",
|
|
1208
|
+
"note": "Added with the marquee component. The second group is a visual copy for the seamless loop, so assistive technology must skip it.",
|
|
1209
|
+
"rules": [
|
|
1210
|
+
{
|
|
1211
|
+
"id": "marquee-copy-is-hidden",
|
|
1212
|
+
"kind": "forbid",
|
|
1213
|
+
"selector": ".marquee > .marquee-content ~ .marquee-content:not([aria-hidden=\"true\"])",
|
|
1214
|
+
"fragmentSafe": true,
|
|
1215
|
+
"message": "The second .marquee-content repeats the first so the loop has no gap. Mark it aria-hidden=\"true\" and give each link or button in it tabindex=\"-1\", or a screen reader reads every item twice."
|
|
1216
|
+
}
|
|
1217
|
+
]
|
|
1218
|
+
},
|
|
1219
|
+
"tree": {
|
|
1220
|
+
"status": "enforced",
|
|
1221
|
+
"note": "Added with the tree component. Nested lists of native disclosures and links: every row is a Tab stop and the browser opens a branch, so no tree role is promised.",
|
|
1222
|
+
"rejects": {
|
|
1223
|
+
"rejected_role": "tree",
|
|
1224
|
+
"because": "its rows are native disclosures and links reached with Tab - a tree role promises arrow-key navigation, type-ahead and expanded states on treeitems that the markup does not have and that native details would fight",
|
|
1225
|
+
"rule": "tree-is-not-a-tree-widget"
|
|
1226
|
+
},
|
|
1227
|
+
"rules": [
|
|
1228
|
+
{
|
|
1229
|
+
"id": "tree-is-not-a-tree-widget",
|
|
1230
|
+
"kind": "forbid",
|
|
1231
|
+
"selector": ".tree[role], .tree [role=\"treeitem\"], .tree [role=\"group\"]",
|
|
1232
|
+
"fragmentSafe": true,
|
|
1233
|
+
"message": "A .tree takes no ARIA tree roles. It is lists of disclosures and links, which keep their own roles and keyboard behavior."
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"id": "tree-marks-current-not-selected",
|
|
1237
|
+
"kind": "forbid",
|
|
1238
|
+
"selector": ".tree [aria-selected]",
|
|
1239
|
+
"fragmentSafe": true,
|
|
1240
|
+
"message": "Mark the open file or page with aria-current. aria-selected is not valid on a link or a list item."
|
|
1241
|
+
}
|
|
1242
|
+
]
|
|
1243
|
+
},
|
|
1244
|
+
"command-palette": {
|
|
1245
|
+
"status": "enforced",
|
|
1246
|
+
"note": "Added with the command palette component. A native modal dialog; the component makes the input a combobox and the list a listbox of options at runtime and owns the active option.",
|
|
1247
|
+
"rules": [
|
|
1248
|
+
{
|
|
1249
|
+
"id": "command-palette-is-a-dialog",
|
|
1250
|
+
"kind": "forbid",
|
|
1251
|
+
"selector": ".command-palette:not(dialog)",
|
|
1252
|
+
"fragmentSafe": true,
|
|
1253
|
+
"message": "Write the palette as <dialog class=\"command-palette\">. The dialog makes it modal, closes it on Escape and returns focus."
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
"id": "command-palette-is-named",
|
|
1257
|
+
"kind": "require-accessible-name",
|
|
1258
|
+
"selector": "dialog.command-palette",
|
|
1259
|
+
"message": "Name the palette with aria-label so it is announced when it opens."
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
"id": "command-palette-input-is-named",
|
|
1263
|
+
"kind": "require-accessible-name",
|
|
1264
|
+
"selector": "dialog.command-palette > input",
|
|
1265
|
+
"message": "Name the search input with aria-label. A placeholder disappears as the user types and is not a reliable name."
|
|
1266
|
+
}
|
|
1267
|
+
]
|
|
1268
|
+
},
|
|
1269
|
+
"questionnaire": {
|
|
1270
|
+
"status": "enforced",
|
|
1271
|
+
"note": "Added with the questionnaire component. A native form of fieldsets; the component shows one question at a time and owns the progress row, hidden questions, invalid state and focus.",
|
|
1272
|
+
"rules": [
|
|
1273
|
+
{
|
|
1274
|
+
"id": "questionnaire-is-a-form",
|
|
1275
|
+
"kind": "forbid",
|
|
1276
|
+
"selector": ".questionnaire:not(form)",
|
|
1277
|
+
"fragmentSafe": true,
|
|
1278
|
+
"message": "Write the questionnaire as <form class=\"questionnaire\">. The form sends the answers, runs the browser's validation and still works without JavaScript."
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
"id": "questionnaire-is-named",
|
|
1282
|
+
"kind": "require-accessible-name",
|
|
1283
|
+
"selector": "form.questionnaire",
|
|
1284
|
+
"message": "Name the questionnaire with aria-label or aria-labelledby so it is announced as a form."
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
"id": "questionnaire-navigation-does-not-submit",
|
|
1288
|
+
"kind": "forbid",
|
|
1289
|
+
"selector": ".questionnaire > footer > button:is([value=previous], [value=skip], [value=next]):not([type=button])",
|
|
1290
|
+
"fragmentSafe": true,
|
|
1291
|
+
"message": "Give the Back, Skip and Next buttons type=\"button\". A button without a type submits the form, so pressing one before the component starts would send it early."
|
|
1292
|
+
}
|
|
1293
|
+
]
|
|
1294
|
+
},
|
|
1295
|
+
"radar-chart": {
|
|
1296
|
+
"status": "enforced",
|
|
1297
|
+
"note": "Added with the radar chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn shapes, labels and legend are aria-hidden.",
|
|
1298
|
+
"rules": [
|
|
1299
|
+
{
|
|
1300
|
+
"id": "radar-chart-is-a-figure-with-a-table",
|
|
1301
|
+
"kind": "forbid",
|
|
1302
|
+
"selector": ".radar-chart:not(figure:has(> table))",
|
|
1303
|
+
"message": "Write .radar-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
"id": "radar-chart-has-a-caption",
|
|
1307
|
+
"kind": "forbid",
|
|
1308
|
+
"selector": ".radar-chart:not(:has(> figcaption))",
|
|
1309
|
+
"message": "Name a .radar-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1310
|
+
}
|
|
1311
|
+
]
|
|
1312
|
+
},
|
|
1313
|
+
"pie-chart": {
|
|
1314
|
+
"status": "enforced",
|
|
1315
|
+
"note": "Added with the pie chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn pie, values, total and legend are aria-hidden.",
|
|
1316
|
+
"rules": [
|
|
1317
|
+
{
|
|
1318
|
+
"id": "pie-chart-is-a-figure-with-a-table",
|
|
1319
|
+
"kind": "forbid",
|
|
1320
|
+
"selector": ".pie-chart:not(figure:has(> table))",
|
|
1321
|
+
"message": "Write .pie-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
"id": "pie-chart-has-a-caption",
|
|
1325
|
+
"kind": "forbid",
|
|
1326
|
+
"selector": ".pie-chart:not(:has(> figcaption))",
|
|
1327
|
+
"message": "Name a .pie-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1328
|
+
}
|
|
1329
|
+
]
|
|
1330
|
+
},
|
|
1331
|
+
"heatmap-chart": {
|
|
1332
|
+
"status": "enforced",
|
|
1333
|
+
"note": "Added with the heatmap chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn grid, labels and scale are aria-hidden.",
|
|
1334
|
+
"rules": [
|
|
1335
|
+
{
|
|
1336
|
+
"id": "heatmap-chart-is-a-figure-with-a-table",
|
|
1337
|
+
"kind": "forbid",
|
|
1338
|
+
"selector": ".heatmap-chart:not(figure:has(> table))",
|
|
1339
|
+
"message": "Write .heatmap-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
"id": "heatmap-chart-has-a-caption",
|
|
1343
|
+
"kind": "forbid",
|
|
1344
|
+
"selector": ".heatmap-chart:not(:has(> figcaption))",
|
|
1345
|
+
"message": "Name a .heatmap-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1346
|
+
}
|
|
1347
|
+
]
|
|
1348
|
+
},
|
|
1349
|
+
"radial-chart": {
|
|
1350
|
+
"status": "enforced",
|
|
1351
|
+
"note": "Added with the radial chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn rings, total and legend are aria-hidden.",
|
|
1352
|
+
"rules": [
|
|
1353
|
+
{
|
|
1354
|
+
"id": "radial-chart-is-a-figure-with-a-table",
|
|
1355
|
+
"kind": "forbid",
|
|
1356
|
+
"selector": ".radial-chart:not(figure:has(> table))",
|
|
1357
|
+
"message": "Write .radial-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
"id": "radial-chart-has-a-caption",
|
|
1361
|
+
"kind": "forbid",
|
|
1362
|
+
"selector": ".radial-chart:not(:has(> figcaption))",
|
|
1363
|
+
"message": "Name a .radial-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
},
|
|
1367
|
+
"mixed-chart": {
|
|
1368
|
+
"status": "enforced",
|
|
1369
|
+
"note": "Added with the mixed chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn columns, lines, labels and legend are aria-hidden.",
|
|
1370
|
+
"rules": [
|
|
1371
|
+
{
|
|
1372
|
+
"id": "mixed-chart-is-a-figure-with-a-table",
|
|
1373
|
+
"kind": "forbid",
|
|
1374
|
+
"selector": ".mixed-chart:not(figure:has(> table))",
|
|
1375
|
+
"message": "Write .mixed-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"id": "mixed-chart-has-a-caption",
|
|
1379
|
+
"kind": "forbid",
|
|
1380
|
+
"selector": ".mixed-chart:not(:has(> figcaption))",
|
|
1381
|
+
"message": "Name a .mixed-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1382
|
+
}
|
|
1383
|
+
]
|
|
1384
|
+
},
|
|
1385
|
+
"bar-chart": {
|
|
1386
|
+
"status": "enforced",
|
|
1387
|
+
"note": "Added with the bar chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn bars, values, labels and legend are aria-hidden.",
|
|
1388
|
+
"rules": [
|
|
1389
|
+
{
|
|
1390
|
+
"id": "bar-chart-is-a-figure-with-a-table",
|
|
1391
|
+
"kind": "forbid",
|
|
1392
|
+
"selector": ".bar-chart:not(figure:has(> table))",
|
|
1393
|
+
"message": "Write .bar-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
"id": "bar-chart-has-a-caption",
|
|
1397
|
+
"kind": "forbid",
|
|
1398
|
+
"selector": ".bar-chart:not(:has(> figcaption))",
|
|
1399
|
+
"message": "Name a .bar-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1400
|
+
}
|
|
1401
|
+
]
|
|
1402
|
+
},
|
|
1403
|
+
"column-chart": {
|
|
1404
|
+
"status": "enforced",
|
|
1405
|
+
"note": "Added with the column chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn columns, labels and legend are aria-hidden.",
|
|
1406
|
+
"rules": [
|
|
1407
|
+
{
|
|
1408
|
+
"id": "column-chart-is-a-figure-with-a-table",
|
|
1409
|
+
"kind": "forbid",
|
|
1410
|
+
"selector": ".column-chart:not(figure:has(> table))",
|
|
1411
|
+
"message": "Write .column-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
"id": "column-chart-has-a-caption",
|
|
1415
|
+
"kind": "forbid",
|
|
1416
|
+
"selector": ".column-chart:not(:has(> figcaption))",
|
|
1417
|
+
"message": "Name a .column-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1418
|
+
}
|
|
1419
|
+
]
|
|
1420
|
+
},
|
|
1421
|
+
"line-chart": {
|
|
1422
|
+
"status": "enforced",
|
|
1423
|
+
"note": "Added with the line chart component. The <table> is the chart's text alternative and its no-JavaScript fallback; the drawn chart, labels and legend are aria-hidden.",
|
|
1424
|
+
"rules": [
|
|
1425
|
+
{
|
|
1426
|
+
"id": "line-chart-is-a-figure-with-a-table",
|
|
1427
|
+
"kind": "forbid",
|
|
1428
|
+
"selector": ".line-chart:not(figure:has(> table))",
|
|
1429
|
+
"message": "Write .line-chart as a <figure> holding a <table> of the data. The drawn chart is hidden from assistive technology, so the table is all a screen reader reads, and it is what shows without JavaScript."
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
"id": "line-chart-has-a-caption",
|
|
1433
|
+
"kind": "forbid",
|
|
1434
|
+
"selector": ".line-chart:not(:has(> figcaption))",
|
|
1435
|
+
"message": "Name a .line-chart with a <figcaption>. It names the focusable plot as well as the figure; hide it with .visually-hidden when nearby text already names the chart."
|
|
1436
|
+
}
|
|
1437
|
+
]
|
|
1438
|
+
},
|
|
1439
|
+
"skeleton": {
|
|
1440
|
+
"status": "enforced",
|
|
1441
|
+
"note": "Added with the skeleton component. A skeleton is an empty placeholder shape. The region it fills carries aria-busy, and a loading indicator or status reports the wait.",
|
|
1442
|
+
"rules": [
|
|
1443
|
+
{
|
|
1444
|
+
"id": "skeleton-is-empty",
|
|
1445
|
+
"kind": "forbid",
|
|
1446
|
+
"selector": ".skeleton:not(:empty)",
|
|
1447
|
+
"fragmentSafe": true,
|
|
1448
|
+
"message": "Keep a .skeleton empty. Text inside it is read out as if it were content; report the wait through a status such as a loading indicator, and mark the loading region aria-busy=\"true\"."
|
|
851
1449
|
}
|
|
852
1450
|
]
|
|
853
1451
|
},
|