Accessibility
From LimeSurvey Manual
Goal
LimeSurvey aims to meet WCAG 2.2 level AA in both the public survey pages and the administration interface. Many customers need this to meet legal requirements such as EN 301 549 or national accessibility laws.
Two rules apply to all work:
- All question types must stay usable. Do not remove or simplify a question type for everyone; give it an accessible fallback instead (see Survey pages).
- Accessibility is part of every change, not a separate project. A pull request that adds or changes UI must keep it keyboard operable and correctly labelled.
Where the code lives
| Area | Location | Notes |
|---|---|---|
| Admin views | application/views/, widgets in application/extensions/ |
PHP views, Bootstrap 5 |
| Shared admin accessibility scripts | assets/packages/adminbasics/src/accessibility/ |
Loaded on every admin page, see Admin interface |
| Question type markup | application/views/survey/questions/answer/ |
Twig, shared by all survey themes |
| Survey themes | themes/survey/ |
fruity_twentythree is the default theme. bootswatch and fruity extend vanilla. fruity_twentythree extends nothing and uses its own JS/CSS bundle built from assets/survey_themes/fruity_twentythree/, so behaviour can differ between themes.
|
| Question themes | themes/question/ |
For example image_select-listradio |
| React survey editor | editor/ |
Separate build |
Conventions
General
- Use native HTML elements first (
<button>,<label>,<fieldset>,<legend>,<table>with). Only add ARIA roles when no native element fits. - Use real heading elements (
h1–h6) in a logical order. Do not userole="heading"witharia-levelwhen a real heading element can be used. - Use the Bootstrap 5 class
visually-hiddenfor text that only screen readers should read, andvisually-hidden-focusablefor skip links. Do not usesr-only; it is a Bootstrap 3/4 class. - Every form control needs an accessible name: a
<label for>, oraria-labelledbypointing to visible text. Usearia-labelonly when there is no visible text, for example on icon-only buttons. - Hide decorative icons from screen readers with
aria-hidden="true". Do not putaria-hiddenon anything that can receive focus. - Every id referenced by
aria-labelledby,aria-describedbyoraria-controlsmust exist in every theme that renders the markup. - Translate all text read by screen readers (
aria-label, captions, visually hidden text) withgT()/eT(). Use one string with%sandsprintf()instead of concatenating translated parts. - Images need an
altattribute. Usealt=""for decorative images. - If the user lacks permission for an action, do not show the control. If the action is unavailable for another reason, show it as disabled (
disabledattribute, oraria-disabled="true"on elements that cannot takedisabled). - Changing state with a click must also work with the keyboard: Enter for links, Enter and Space for buttons.
- Never remove the focus outline without providing a visible replacement.
Admin interface
The following helpers are loaded on every admin page by the adminbasics package or by the related widget. Use them instead of writing new solutions.
| Need | How |
|---|---|
| Tabs | Mark up tabs with role="tablist", role="tab", aria-selected, aria-controls and role="tabpanel". accessibility/tabsControl.js adds arrow, Home and End key navigation and the roving tabindex for all Bootstrap tabs and pills. After you insert tabs dynamically outside an ajax call, call LS.refreshTabsA11y().
|
| Modals | The modal needs aria-labelledby pointing to the id of the modal title, and aria-modal="true". The close button needs aria-label="<?php eT('Close'); ?>". After the modal closes, focus must go back to the element that opened it.
|
| Data grids | Pass lsCaption to CLSGridView. It is rendered as a visually hidden <caption>. application/extensions/admin/grid/assets/sortAccessibility.js makes sort links work with the keyboard and keeps focus on the sorted column after the ajax reload.
|
| Status and flash messages | Use application/views/layouts/messagebox.php or Yii::app()->user->setFlash(). The message container has role="status", aria-live="polite" and tabindex="-1", and accessibility/statusMessageFocus.js moves focus to it so screen readers announce it after a redirect.
|
| Select2 dropdowns | Give the <select> a <label for> or aria-labelledby. select2-aria.js (loaded by WhSelect2) copies the label to the Select2 combobox and search field and keeps aria-expanded in sync.
|
| Button groups | ButtonGroupWidget takes ariaLabelledBy (preferred) or ariaLabel.
|
| Tooltips | accessibility/dismissTooltipsOnEscapePress.js closes open tooltips with Escape. Never put information only in a tooltip.
|
| Page title | The browser title is built from the topbar title ($aData['topbar']['title']) or the title bar title. Set one on every page so screen reader users can tell pages apart.
|
Survey pages
- The question text has the id
ls-question-text-{SGQ}. Answer containers userole="group"orrole="radiogroup"witharia-labelledbypointing to it. Array rows arelabelled by the row text, and each radio is labelled by its column header. - JavaScript widgets need a fallback that works with screen readers and without JavaScript:
ls-js-hidden-sr: visually hidden when JavaScript is on, still read by screen readers. Used for the native<select>fallback of the ranking question.ls-js-hidden/ls-no-js-hidden: hidden when JavaScript is on / off.- Good example: the 5-point emoji slider puts
aria-hiddenon the visual widget and keeps the real radio buttons asvisually-hiddeninputs.
- Expression Manager relevance adds the classes
ls-irrelevant ls-hidden(display:none) on therelevance:on/relevance:offevents inassets/packages/limesurvey/survey.js(copied for Fruity TwentyThree inassets/survey_themes/fruity_twentythree/core/old_template_core_pre.js). Hidden content is therefore removed from the accessibility tree. - The page title has the form "page part - survey title". The page part is the group name when one group is shown, or a label such as Welcome or Thank you!. The prefix "Error: " is added when the page shows errors.
- Survey administrators can set the Fruity TwentyThree theme option Show popups to On page to show validation errors inline instead of in a modal.
React editor
New components in editor/ must follow the same rules. eslint-plugin-jsx-a11y is installed but not yet enabled in editor/.eslintrc.json.
Testing
Do these checks for every UI change:
- Keyboard only: Reach and use every control with Tab, Shift+Tab, Enter, Space, the arrow keys and Escape. Focus must always be visible and must never get trapped.
- Screen reader: Test with NVDA (Windows, Firefox or Chrome) or VoiceOver (macOS/iOS, Safari). Check that controls are read with their name, role and state, and that dynamic changes (errors, status messages) are announced.
- Automated check: Run axe DevTools or WAVE on the page. These find only part of the problems, so they do not replace steps 1 and 2.
- Zoom and reflow: At 200 % browser zoom and at 320 px width, nothing may be cut off and there may be no horizontal scrolling (except for data tables).
- Contrast: Text needs a contrast ratio of at least 4.5:1, large text and UI components at least 3:1.
Survey authors can check their own question and help texts with the accessibility checker button in the HTML editor (CKEditor a11ychecker plugin).
Known gaps
These problems were found in the code on master in October 2026. Report new problems in the bug tracker and remove items here once they are fixed.
Survey pages
- The ranking and multiple short text questions reference
ls-question-validation-{SGQ}inaria-labelledby, but only Fruity TwentyThree renders an element with that id. In vanilla, bootswatch and fruity the reference points to nothing. - Mandatory questions are announced only by text. No input has
requiredoraria-required. - Inputs with a validation error do not get
aria-invalid. Expression Manager tips change only their CSS class; only Fruity TwentyThree announces the change through a live region. - Help text is not connected to the inputs with
aria-describedby. - Questions that appear or disappear through relevance are not announced.
- After a page is submitted with errors, focus does not move to the first error. The function
focusFirst()in the theme JS exists but its call is commented out. - Only Fruity TwentyThree has a skip link; vanilla and the themes extending it have none.
- The progress bar has
aria-hidden="true"in all themes. - In list radio questions each option is labelled by the full question text plus the option text, so the question is read again for every option.
- The error modal close button (
application/models/TemplateConfig.php) hasaria-hidden="true"and noaria-label. - File upload: the upload modal has no
aria-labelledby/aria-modal, its close button has noaria-label, and the upload status is not announced. - Image select question theme: the image is the only content of the label and has no
alt, so the radio buttons have no accessible name. - Slider (multiple numeric): the reset button has only an icon and a tooltip, no
aria-label. - Date picker: the calendar button is a
and cannot be focused with the keyboard. Typing the date in the text field works. - Short text with map: the map search field has no label.
- Array dual scale (dropdown layout):
role="group"is set on<col>elements, where it has no effect. - None of the Fruity TwentyThree colour variations is a high contrast variation, and the colour options do not check contrast.
Admin interface
BaselineModalWidgetdoes not setaria-labelledbyoraria-modalon the modal.
Tooling
eslint-plugin-jsx-a11yis not enabled for the React editor.- There are no automated accessibility tests in the test suite.
History
This page replaces the planning notes from 2017 for LimeSurvey 2.x/3.x. They are kept in the page history.
References
- WCAG 2.2
- ARIA Authoring Practices Guide (APG): patterns for tabs, dialogs, comboboxes and more
- Bootstrap 5 accessibility
- EU accessibility law and EN 301 549 (W3C overview)
- Code quality guide
- Standard for Git commit messages