Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/community-access/quill-accessible-forms/archive/refs/heads/main.zipQUILL Accessible Forms (QAF)
A fully accessible WordPress form builder plugin with WCAG 2.2 AA compliance, custom form language (the QAF form language), and advanced entry views.
Features
Accessibility First
- WCAG 2.2 AA Compliant - All forms meet accessibility standards out of the box
- Screen Reader Support - Proper ARIA labels, live regions, and announcements
- Keyboard Navigation - Full keyboard support for all interactions
- Focus Management - Intelligent focus trapping and restoration
- High Contrast Mode - Automatic support for high contrast preferences
- Reduced Motion - Respects
prefers-reduced-motionsettings
Visual Form Builder
- Drag & Drop Interface - Build forms visually with 30+ field types
- Structure Mode - Code-first approach using the QAF form language
- Real-time Preview - See changes as you make them
- Conditional Logic - Show/hide fields based on user input
- Multi-page Forms - Create wizard-style multi-step forms
- Input Masking - Guide user input formats (e.g. phone, SSN)
- Smart Defaults - Auto-fill fields (
geo,today+N,user:*) - Unique Validation - Enforce uniqueness with per-form or global scope
- Address Autocomplete - Optional Google Places integration
Field Types
| Basic | Advanced | Payment |
|---|---|---|
| Text | File Upload | Stripe |
| Signature | PayPal | |
| Number | Rating | Calculation |
| Textarea | Likert Scale | |
| Select | NPS Score | |
| Radio | Ranking | |
| Checkbox | Repeater | |
| Date/Time | Address | |
| Phone | Name | |
| URL | Hidden | |
| Password | HTML | |
| Color | Section | |
| Range | Page Break |
The QAF form language
Define forms as code with our custom domain-specific language.
form "Contact Us"
@submit_label "Send Message"
@success_message "Thank you for contacting us!"
text "Your Name"
@id "name"
@required
@placeholder "John Doe"
email "Email Address"
@id "email"
@required
textarea "Your Message"
@id "message"
@required
@min 20
@max 1000
@rows 6
submit "Send Message"
Phase 5 Enhancements (implemented):
text "Phone"
@id "phone"
@mask "(999) 999-9999"
address "Shipping Address"
@id "ship_address"
@autocomplete
@autocomplete_provider "google_places"
@autocomplete_countries ["US","CA"]
@autocomplete_types ["address"]
text "Name"
@id "display_name"
@smart_default "user:display_name"
email "Email"
@id "email_unique"
@unique
@unique_scope "form" # form|global
@unique_message "This email is already registered"
Address autocomplete requires a Google Maps API key configured under QAF Forms → Settings → Integrations.
Choice Libraries
Use predefined choice lists instead of typing options manually. QAF includes 125+ built-in libraries with full cascading support:
select country {
label "Country"
library countries
}
select state {
label "State/Province"
cascade_from country
cascade_library states // Auto-populates states based on country
empty_message "Please select a country first"
}
Geographic Libraries (35+):
| Library | Description |
|---|---|
countries |
All 250+ countries (ISO 3166-1) |
continents |
7 continents |
world_regions |
Geographic regions (Americas, Europe, Asia, etc.) |
us_states |
US states + DC + territories |
ca_provinces |
Canadian provinces and territories |
uk_counties |
UK ceremonial counties (~45 regions) |
au_states |
Australian states and territories |
de_states |
German Bundesländer (16) |
fr_regions |
French regions (13) |
it_regions |
Italian regions (20) |
es_regions |
Spanish autonomous communities (19) |
nl_provinces |
Dutch provinces (12) |
be_provinces |
Belgian provinces (11) |
ch_cantons |
Swiss cantons (26) |
at_states |
Austrian states (9) |
mx_states |
Mexican states (32) |
br_states |
Brazilian states (27) |
ar_provinces |
Argentine provinces (24) |
in_states |
Indian states and territories (36) |
cn_provinces |
Chinese provinces (34) |
jp_prefectures |
Japanese prefectures (47) |
kr_provinces |
South Korean provinces (17) |
nz_regions |
New Zealand regions (16) |
za_provinces |
South African provinces (9) |
ie_counties |
Irish counties (26) |
pl_voivodeships |
Polish voivodeships (16) |
ru_regions |
Russian major regions (20) |
cities_us |
Major US cities (40) |
cities_world |
Major world cities (48) |
Time & Date Libraries:
| Library | Description |
|---|---|
timezones |
All IANA timezones |
timezones_us |
US timezones only |
timezones_* |
By region (europe, asia, america, etc.) |
days |
Days of the week |
months |
Months of the year |
years_past |
Past 100 years |
years_future |
Next 10 years |
years_birth |
Birth years (1920-present) |
time_slots_hourly |
Hourly time slots |
time_slots_30min |
30-minute intervals |
time_slots_15min |
15-minute intervals |
Financial Libraries:
| Library | Description |
|---|---|
currencies |
58 world currencies (ISO 4217) |
currencies_major |
8 major currencies (USD, EUR, GBP, etc.) |
cryptocurrencies |
12 popular cryptocurrencies |
budget_ranges |
Budget brackets ($1K - $1M+) |
annual_revenue |
Company revenue ranges |
payment_methods |
Credit card, PayPal, etc. |
Language & Contact Libraries:
| Library | Description |
|---|---|
languages |
50 world languages (ISO 639-1) |
languages_major |
12 major languages |
phone_codes |
51 country dialing codes |
contact_preferences |
Email, phone, SMS, etc. |
contact_times |
Morning, afternoon, evening |
Demographic Libraries:
| Library | Description |
|---|---|
age_ranges |
Age brackets |
gender |
Gender options (inclusive) |
marital_status |
Marital status options |
education_levels |
Education attainment |
employment_status |
Employment types |
income_ranges |
Income brackets |
household_sizes |
1-7+ people |
religions |
World religions |
ethnicities |
Ethnicity options |
relationship_statuses |
Relationship status |
Professional Libraries:
| Library | Description |
|---|---|
industries |
60+ industry sectors |
job_functions |
Job departments/functions |
experience_levels |
Entry to C-Level |
years_experience |
Experience ranges |
company_sizes |
Company employee counts |
fields_of_study |
Academic majors |
lead_sources |
How did you hear about us |
project_types |
Project categories |
timelines |
Timeline options |
E-commerce Libraries:
| Library | Description |
|---|---|
tshirt_sizes |
XS to 5XL |
shoe_sizes_us |
US shoe sizes |
shoe_sizes_eu |
EU shoe sizes |
product_conditions |
New, used, refurbished |
colors |
Common color options |
quantities |
1-100 quantity selector |
shipping_methods |
Standard, express, overnight |
subscription_frequencies |
Weekly to annually |
Rating & Feedback Libraries:
| Library | Description |
|---|---|
rating_5_star |
1-5 star rating |
satisfaction |
Very unsatisfied to very satisfied |
agreement |
Strongly disagree to strongly agree |
likelihood |
Very unlikely to very likely |
frequency |
Never to always |
quality |
Poor to excellent |
priority_levels |
Low to critical |
Health & Medical Libraries:
| Library | Description |
|---|---|
blood_types |
A+, A-, B+, etc. |
medical_specialties |
30+ medical specialties |
dietary_restrictions |
Vegetarian, vegan, gluten-free, etc. |
allergens |
Common food allergens |
accessibility_needs |
Wheelchair, hearing, visual, etc. |
appointment_types |
Consultation, follow-up, etc. |
Technology Libraries:
| Library | Description |
|---|---|
platforms |
Windows, macOS, Linux, iOS, Android |
device_types |
Desktop, laptop, tablet, smartphone |
browsers |
Chrome, Firefox, Safari, Edge |
skill_levels |
Beginner to expert |
Boolean & Simple Libraries:
| Library | Description |
|---|---|
yes_no |
Yes/No |
yes_no_unsure |
Yes/No/Not sure |
true_false |
True/False |
agree_disagree |
Agree/Disagree |
Cascading Support:
Libraries automatically cascade when related:
continent→countries(filter countries by continent)country→states(auto-detect correct state/province list)timezone_region→timezones(filter by region)
Register Custom Libraries:
add_action('qaf_register_choice_libraries', function() {
QAF_Choice_Libraries::register('departments', array(
'label' => 'Departments',
'choices' => array(
array('value' => 'sales', 'label' => 'Sales'),
array('value' => 'support', 'label' => 'Support'),
array('value' => 'engineering', 'label' => 'Engineering'),
),
));
});
Entry Views
Display form submissions in multiple layouts:
- Table View - Sortable, filterable data table with bulk actions
- Cards View - Visual card layout with images and badges
- List View - Compact list format for quick scanning
- Map View - Geographic visualization for location data
- Calendar View - Date-based entry display
Payments
A form with a payment field takes money through Stripe, PayPal or both. This is the side of the payment contract (version 1) that QUILL and QUILL Lite publish to; the full guide for site owners is docs/payments-admin-guide.md.
- Keys stay on the site. You enter them under QAF Forms > Payments, or
in
wp-config.php. The screen never shows a secret again, no REST route returns one, and the payment page gets only the publishable key or client ID. - The site works out what is owed. Fixed amounts, donations with a smallest and largest gift, and totals made of products, quantities, coupons, shipping and tax are all recomputed on the server from the form itself, in whole cents rather than floating point. Nothing the browser says about money is believed.
- Stripe Payment Intents and PayPal Orders v2, created on the server with idempotency keys, so a double click or a resent form cannot charge twice.
- Signed webhooks. Stripe's signatures are checked with the signing secret, PayPal's with PayPal's verification call. Each event counts once, must match the payment's environment, amount and currency, and can never move a paid or refunded entry backwards.
- Test mode and live mode for the whole site, with a check box to confirm before going live. A test form never takes money on a live site, and a live form is refused on a test site.
- Entry status (waiting, authorized, paid, failed, cancelled, partly refunded, refunded) in the entries REST reply and a payments report with totals per currency.
- Accessible checkout: labelled fields, errors tied to their field, a heading that takes focus when the payment panel appears, one status region and one alert, a keyboard-only flow, and nothing shown by colour alone.
- Fulfilment hook:
qaf_payment_paidfires once per paid entry.
Integrations
- Stripe and PayPal - see Payments above
- Google Sheets - Auto-sync entries to spreadsheets
- Webhooks - Custom API integrations
- Email - Notification and autoresponder emails
- PDF Generation - Generate PDFs from entries
Security
- CSRF Protection - All forms protected against cross-site request forgery
- Honeypot Fields - Invisible bot detection
- Rate Limiting - Prevent form spam and abuse
- CAPTCHA Support - reCAPTCHA v2/v3 and hCaptcha
- Input Sanitization - Comprehensive XSS and injection protection
- File Validation - Secure file upload handling
Installation
From WordPress Admin
- Go to Plugins → Add New
- Search for "QUILL Accessible Forms"
- Click Install Now and then Activate
Manual Installation
- Download the plugin ZIP file
- Go to Plugins → Add New → Upload Plugin
- Upload the ZIP file and click Install Now
- Activate the plugin
Via Composer
composer require bits/accessible-forms
Upgrading from BITS Accessible Forms
QUILL Accessible Forms is BITS Accessible Forms under its new name. Install it and activate it; the old plugin can then be deleted. Your forms and entries come across by migration, once, when QUILL Accessible Forms is activated (or the first time an administrator opens the dashboard after the files are replaced):
- each
baf_table is renamed toqaf_, with its rows; - each
baf_option is copied toqaf_, unless the new one already exists. The old options are left in place for one release, so going back to the old plugin still works; baf_and_baf_user, post and comment meta keys becomeqaf_and_qaf_;- every role that had a
baf_capability is given theqaf_one.
Nothing has to be rewritten by hand. For one release:
- pages that use
[baf_form]or[baf_entries]keep working; - the REST namespace
baf/v1answers GET requests asqaf/v1does, and says that it is deprecated; - form files saved as
.bflstill open. New ones are saved as.qaf.
Uploaded files stay where they are. What the migration did is kept in the
qaf_migrated_from_baf option.
Requirements
- WordPress 6.0 or higher
- PHP 8.0 or higher
- MySQL 5.7+ or MariaDB 10.3+
Quick Start
Create Your First Form
- Go to QAF Forms → Add New
- Enter a form title
- Drag fields from the sidebar to build your form
- Configure field settings in the right panel
- Click Save Form
Display a Form
Use the shortcode:
[qaf_form id="1"]
Or the Gutenberg block:
- Add a new block
- Search for "QAF Form"
- Select your form from the dropdown
View Entries
- Go to QAF Forms → Entries
- Select a form to view its submissions
- Use filters and search to find specific entries
- Export data as CSV, Excel, or PDF
Configuration
Global Settings
Navigate to QAF Forms → Settings to configure:
- Default form styles
- Email notifications
- Payment gateway credentials
- reCAPTCHA keys
- Integration API keys
Form-Specific Settings
Each form has its own settings panel:
- General - Title, description, status
- Submissions - Entry handling, limits, scheduling
- Notifications - Admin and user emails
- Confirmations - Success messages and redirects
- Payments - Payment field configuration
- Integrations - Third-party connections
API Reference
REST API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/qaf/v1/forms |
GET | List all forms |
/qaf/v1/forms/{id} |
GET | Get single form |
/qaf/v1/forms |
POST | Create form |
/qaf/v1/forms/{id} |
PUT | Update form |
/qaf/v1/forms/{id} |
DELETE | Delete form |
/qaf/v1/entries |
GET | List entries |
/qaf/v1/entries/{id} |
GET | Get single entry |
/qaf/v1/entries |
POST | Create entry |
/qaf/v1/bfl/parse |
POST | Parse QAF form language code |
/qaf/v1/bfl/validate |
POST | Validate QAF form language code |
/qaf/v1/payments/settings |
GET | Payment readiness (contract version 1); administrators only, never a key |
/qaf/v1/payments/start |
POST | Checkout: start or resume a payment (needs the submission's token) |
/qaf/v1/payments/finish |
POST | Checkout: capture an approved PayPal order (needs the token) |
/qaf/v1/payments/status |
POST | Checkout: the site asks the provider and answers (needs the token) |
/qaf/v1/payments/webhook/stripe |
POST | Stripe events, signature checked |
/qaf/v1/payments/webhook/paypal |
POST | PayPal events, verified with PayPal |
PHP Hooks
Actions
// Before form renders
do_action( 'qaf_before_form_render', $form_id, $form );
// After successful submission
do_action( 'qaf_after_submission', $entry_id, $form_id, $data );
// Before entry is saved
do_action( 'qaf_before_entry_save', $data, $form_id );
// After payment processed
do_action( 'qaf_payment_completed', $entry_id, $payment_data );
Filters
// Modify form HTML
add_filter( 'qaf_form_html', function( $html, $form_id ) {
return $html;
}, 10, 2 );
// Validate submission data
add_filter( 'qaf_validate_submission', function( $errors, $data, $form_id ) {
return $errors;
}, 10, 3 );
// Modify field options dynamically
add_filter( 'qaf_field_options', function( $options, $field, $form_id ) {
return $options;
}, 10, 3 );
// Customize email content
add_filter( 'qaf_notification_email', function( $email, $entry, $form ) {
return $email;
}, 10, 3 );
JavaScript API
// Get form instance
const form = QAF.getForm('form-id');
// Submit programmatically
form.submit();
// Validate all fields
const isValid = form.validate();
// Get/set field values
form.getField('email').getValue();
form.getField('email').setValue('test@example.com');
// Listen to events
form.on('submit', (data) => console.log(data));
form.on('validation:error', (errors) => console.log(errors));
form.on('field:change', (field, value) => console.log(field, value));
Testing
Run PHPUnit Tests
./vendor/bin/phpunit
The payment tests (tests/unit/Payments/) run without WordPress or a
network: every Stripe and PayPal reply in them is written by hand from the
providers' documented shapes and served by a fake transport, and the keys
are placeholders. They never charge anything. Live sandbox checks are a
separate, manual step (see the admin guide).
Run JavaScript Tests
npm test
Code Coverage
./vendor/bin/phpunit --coverage-html coverage/
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Development Setup
- Clone the repository
- Run
composer install - Run
npm install - Set up local WordPress environment
- Symlink or copy plugin to
wp-content/plugins/
Coding Standards
- Follow WordPress Coding Standards
- Use PHP 8.0+ features appropriately
- Ensure WCAG 2.2 AA compliance for all UI changes
- Write tests for new functionality
Changelog
See CHANGELOG.md for version history.
License
This plugin is licensed under the MIT License; see LICENSE. MIT is compatible with the GPL, so the plugin can be distributed through WordPress.org and used alongside GPL themes and plugins.
Support
- Documentation: docs.bitsforms.com
- GitHub Issues: Report bugs
- Community Forum: WordPress.org Support
Credits
Built by Jeff Bishop.
Special thanks to:
- WordPress community
- WAI-ARIA working group
- All our contributors and testers