label */ public static function standards() { return array( 'no-private' => __( 'Norway – private sector (35 WCAG 2.0 criteria)', 'ariaccess' ), 'no-public' => __( 'Norway – public sector (47 WCAG 2.1 criteria)', 'ariaccess' ), 'wcag21aa' => __( 'WCAG 2.1 level AA (EN 301 549, EU)', 'ariaccess' ), 'wcag22aa' => __( 'WCAG 2.2 level AA', 'ariaccess' ), ); } /** * The criteria in a standard. * * @param string $standard Standard key. * @return string[] */ public static function criteria( $standard ) { switch ( $standard ) { case 'no-private': return array_values( array_diff( self::WCAG20, self::NO_EXEMPT ) ); case 'no-public': return array_values( array_diff( array_merge( self::WCAG20, self::WCAG21_ADDED ), self::NO_EXEMPT ) ); case 'wcag22aa': return array_values( array_diff( array_merge( self::WCAG20, self::WCAG21_ADDED, self::WCAG22_ADDED ), array( '4.1.1' ) ) ); case 'wcag21aa': default: return array_merge( self::WCAG20, self::WCAG21_ADDED ); } } /** * Whether a rule counts as a failure under a standard (otherwise it is advice). * * @param array $rule Rule. * @param string $standard Standard key. * @return bool */ public static function in_standard( $rule, $standard ) { return (bool) array_intersect( $rule['wcag'], self::criteria( $standard ) ); } /** * W3C "Understanding" page for a criterion. * * @param string $sc Success criterion number. * @return string */ public static function understanding_url( $sc ) { $slugs = array( '1.1.1' => 'non-text-content', '1.2.1' => 'audio-only-and-video-only-prerecorded', '1.2.2' => 'captions-prerecorded', '1.3.1' => 'info-and-relationships', '1.4.2' => 'audio-control', '1.4.3' => 'contrast-minimum', '1.4.4' => 'resize-text', '2.2.2' => 'pause-stop-hide', '2.4.3' => 'focus-order', '2.4.4' => 'link-purpose-in-context', '2.4.6' => 'headings-and-labels', '3.2.5' => 'change-on-request', '3.3.2' => 'labels-or-instructions', '4.1.1' => 'parsing', '4.1.2' => 'name-role-value', ); return isset( $slugs[ $sc ] ) ? 'https://www.w3.org/WAI/WCAG21/Understanding/' . $slugs[ $sc ] . '.html' : 'https://www.w3.org/WAI/WCAG21/quickref/'; } /** * Rule catalogue for the content checker. * * Types: error (very likely a barrier), warning (likely, needs a look), notice (check by hand). * * @return array id => rule */ public static function all() { static $rules = null; if ( null !== $rules ) { return $rules; } $rules = array( 'img_alt_missing' => array( 'title' => __( 'Image has no alt attribute', 'ariaccess' ), 'wcag' => array( '1.1.1' ), 'type' => 'error', 'why' => __( 'Screen readers announce the file name or skip the image, so its meaning is lost.', 'ariaccess' ), 'fix' => __( 'Describe what the image shows or does. If it is purely decorative, give it an empty alt (alt="") so screen readers skip it.', 'ariaccess' ), ), 'img_alt_suspicious' => array( 'title' => __( 'Alt text looks like a file name or placeholder', 'ariaccess' ), 'wcag' => array( '1.1.1' ), 'type' => 'warning', 'why' => __( 'Alt text such as "IMG_2041.jpg" or "image" tells the listener nothing.', 'ariaccess' ), 'fix' => __( 'Replace it with a short description of the content or purpose of the image.', 'ariaccess' ), ), 'img_alt_long' => array( 'title' => __( 'Alt text is very long', 'ariaccess' ), 'wcag' => array( '1.1.1' ), 'type' => 'notice', 'why' => __( 'Alt text over about 150 characters is tiring to listen to and cannot be skipped.', 'ariaccess' ), 'fix' => __( 'Keep alt text short. Put long descriptions (charts, diagrams) in the text or a caption.', 'ariaccess' ), ), 'link_image_no_text' => array( 'title' => __( 'Linked image has no text alternative', 'ariaccess' ), 'wcag' => array( '1.1.1', '2.4.4' ), 'type' => 'error', 'why' => __( 'The link has no name, so screen reader users hear only "link" and cannot tell where it goes.', 'ariaccess' ), 'fix' => __( 'Give the image alt text that describes where the link goes, or add visible link text.', 'ariaccess' ), ), 'link_empty' => array( 'title' => __( 'Empty link', 'ariaccess' ), 'wcag' => array( '2.4.4', '4.1.2' ), 'type' => 'error', 'why' => __( 'A link with no text is announced without a name and is confusing for keyboard users.', 'ariaccess' ), 'fix' => __( 'Add link text, or remove the empty link.', 'ariaccess' ), ), 'link_ambiguous' => array( 'title' => __( 'Link text does not say where it goes', 'ariaccess' ), 'wcag' => array( '2.4.4' ), 'type' => 'warning', 'why' => __( 'Screen reader users often browse a list of links. "Click here" or "Les mer" means nothing out of context.', 'ariaccess' ), 'fix' => __( 'Rewrite the link so it describes the destination, for example "Read the 2026 price list".', 'ariaccess' ), ), 'link_url_text' => array( 'title' => __( 'Link text is a web address', 'ariaccess' ), 'wcag' => array( '2.4.4' ), 'type' => 'notice', 'why' => __( 'Long addresses are read out character by character.', 'ariaccess' ), 'fix' => __( 'Use a short description as link text instead of the address.', 'ariaccess' ), ), 'link_new_window' => array( 'title' => __( 'Link opens a new window without warning', 'ariaccess' ), 'wcag' => array( '3.2.5' ), 'type' => 'notice', 'why' => __( 'A new window breaks the Back button and can disorient screen reader and magnifier users.', 'ariaccess' ), 'fix' => __( 'Open links in the same window, or say "(opens in a new tab)" in the link. AriAccess can add the warning for you under Fixes.', 'ariaccess' ), ), 'link_document' => array( 'title' => __( 'Link to a PDF or office document', 'ariaccess' ), 'wcag' => array(), 'type' => 'notice', 'why' => __( 'Documents must be accessible too, and users should know a link downloads a file.', 'ariaccess' ), 'fix' => __( 'Check the document is tagged and readable, or publish the content as a web page. Mention the file type in the link text.', 'ariaccess' ), ), 'heading_empty' => array( 'title' => __( 'Empty heading', 'ariaccess' ), 'wcag' => array( '1.3.1', '2.4.6' ), 'type' => 'error', 'why' => __( 'Screen reader users jump between headings; an empty one is announced as "heading" with no name.', 'ariaccess' ), 'fix' => __( 'Add heading text or delete the empty heading block.', 'ariaccess' ), ), 'heading_skipped' => array( 'title' => __( 'Heading level skipped', 'ariaccess' ), 'wcag' => array( '1.3.1' ), 'type' => 'warning', 'why' => __( 'Jumping from H2 to H4 suggests a missing section and breaks the outline.', 'ariaccess' ), 'fix' => __( 'Use heading levels in order. Change the style with the editor, not the level.', 'ariaccess' ), ), 'heading_missing' => array( 'title' => __( 'Long text without headings', 'ariaccess' ), 'wcag' => array( '2.4.6', '1.3.1' ), 'type' => 'notice', 'why' => __( 'Headings let everyone, and especially screen reader users, scan and jump through long text.', 'ariaccess' ), 'fix' => __( 'Break the text into sections with descriptive headings.', 'ariaccess' ), ), 'heading_fake' => array( 'title' => __( 'Bold paragraph that may be a heading', 'ariaccess' ), 'wcag' => array( '1.3.1' ), 'type' => 'notice', 'why' => __( 'Text that only looks like a heading is not announced as one.', 'ariaccess' ), 'fix' => __( 'If it introduces a section, turn it into a real heading block.', 'ariaccess' ), ), 'table_no_header' => array( 'title' => __( 'Data table without header cells', 'ariaccess' ), 'wcag' => array( '1.3.1' ), 'type' => 'error', 'why' => __( 'Without header cells, screen readers cannot say which column or row a value belongs to.', 'ariaccess' ), 'fix' => __( 'Turn on "Header section" in the table block, or mark the first row as header cells (th).', 'ariaccess' ), ), 'table_empty_header' => array( 'title' => __( 'Empty table header cell', 'ariaccess' ), 'wcag' => array( '1.3.1' ), 'type' => 'warning', 'why' => __( 'An empty header gives the cells below it no label.', 'ariaccess' ), 'fix' => __( 'Add text to the header cell, or make it a normal cell (td).', 'ariaccess' ), ), 'iframe_no_title' => array( 'title' => __( 'Embedded frame has no title', 'ariaccess' ), 'wcag' => array( '4.1.2' ), 'type' => 'error', 'why' => __( 'Screen readers announce untitled frames as "frame", so users cannot tell what the embed is.', 'ariaccess' ), 'fix' => __( 'Add a title attribute that describes the embed, for example "Map: our office in Oslo".', 'ariaccess' ), ), 'media_autoplay' => array( 'title' => __( 'Audio or video plays automatically', 'ariaccess' ), 'wcag' => array( '1.4.2', '2.2.2' ), 'type' => 'error', 'why' => __( 'Sound that starts on its own drowns out screen readers; moving video distracts.', 'ariaccess' ), 'fix' => __( 'Turn off autoplay, or make it muted with visible controls.', 'ariaccess' ), ), 'video_no_captions' => array( 'title' => __( 'Video may be missing captions', 'ariaccess' ), 'wcag' => array( '1.2.2' ), 'type' => 'warning', 'why' => __( 'Deaf and hard-of-hearing visitors need captions to follow speech in video.', 'ariaccess' ), 'fix' => __( 'Add a captions track (VTT) to the video block, or turn on captions on YouTube/Vimeo and check they are correct.', 'ariaccess' ), ), 'audio_transcript' => array( 'title' => __( 'Audio needs a transcript', 'ariaccess' ), 'wcag' => array( '1.2.1' ), 'type' => 'notice', 'why' => __( 'Podcasts and audio clips need a text version for people who cannot hear them.', 'ariaccess' ), 'fix' => __( 'Publish a transcript next to the audio player.', 'ariaccess' ), ), 'form_no_label' => array( 'title' => __( 'Form field without a label', 'ariaccess' ), 'wcag' => array( '1.3.1', '3.3.2', '4.1.2' ), 'type' => 'error', 'why' => __( 'Without a label, screen readers announce only "edit text"; placeholder text disappears when typing.', 'ariaccess' ), 'fix' => __( 'Give every field a visible label connected with for/id, or at least an aria-label.', 'ariaccess' ), ), 'button_empty' => array( 'title' => __( 'Button without a name', 'ariaccess' ), 'wcag' => array( '4.1.2' ), 'type' => 'error', 'why' => __( 'Icon-only buttons with no text are announced only as "button".', 'ariaccess' ), 'fix' => __( 'Add button text, or an aria-label describing the action.', 'ariaccess' ), ), 'contrast_low' => array( 'title' => __( 'Text colour contrast too low', 'ariaccess' ), 'wcag' => array( '1.4.3' ), 'type' => 'error', 'why' => __( 'Pale text on a pale background is hard to read for people with low vision, and for everyone in sunlight.', 'ariaccess' ), 'fix' => __( 'Use colours with a contrast ratio of at least 4.5:1 (3:1 for large text). Try the contrast checker under AriAccess → Tools.', 'ariaccess' ), ), 'blink_marquee' => array( 'title' => __( 'Blinking or scrolling text', 'ariaccess' ), 'wcag' => array( '2.2.2' ), 'type' => 'error', 'why' => __( 'Moving text cannot be paused and is hard or impossible to read for many people.', 'ariaccess' ), 'fix' => __( 'Remove the blink or marquee element.', 'ariaccess' ), ), 'duplicate_id' => array( 'title' => __( 'Duplicate id attribute', 'ariaccess' ), 'wcag' => array( '4.1.1' ), 'type' => 'warning', 'why' => __( 'Labels, skip links and ARIA references break when two elements share an id.', 'ariaccess' ), 'fix' => __( 'Give each element a unique id (check HTML anchors on blocks).', 'ariaccess' ), ), 'tabindex_positive' => array( 'title' => __( 'Positive tabindex changes the focus order', 'ariaccess' ), 'wcag' => array( '2.4.3' ), 'type' => 'warning', 'why' => __( 'Keyboard users jump around the page in an unexpected order.', 'ariaccess' ), 'fix' => __( 'Remove tabindex values above 0. AriAccess can do this for you under Fixes.', 'ariaccess' ), ), 'text_small' => array( 'title' => __( 'Very small text', 'ariaccess' ), 'wcag' => array(), 'type' => 'notice', 'why' => __( 'Text under 12px is hard to read, especially on phones.', 'ariaccess' ), 'fix' => __( 'Use a larger font size for body text.', 'ariaccess' ), ), ); /** * Filter the content-checker rule catalogue. * * @param array $rules id => rule. */ $rules = apply_filters( 'ariaccess_rules', $rules ); return $rules; } /** * One rule. * * @param string $id Rule id. * @return array|null */ public static function get( $id ) { $all = self::all(); return isset( $all[ $id ] ) ? $all[ $id ] : null; } /** * Labels for issue types. * * @return array */ public static function type_labels() { return array( 'error' => __( 'Must fix', 'ariaccess' ), 'warning' => __( 'Should review', 'ariaccess' ), 'notice' => __( 'Check by hand', 'ariaccess' ), ); } }