diff --git a/docs/javascript.md b/docs/javascript.md
index e651c399..5eff3a5c 100644
--- a/docs/javascript.md
+++ b/docs/javascript.md
@@ -398,3 +398,41 @@ GOVUK.selectionButtons = function (elms, opts) {
```
This method will mean the `destroy` method is not available to call.
+
+## Show/Hide content
+
+Script to support show/hide content, toggled by radio buttons and checkboxes. This allows for progressive disclosure of question and answer forms based on selected values:
+
+
+
+
+
+
+
Show/Hide content to be toggled
+
+
+When the input's `checked` attribute is set, the show/hide content's `.js-hidden` class is removed and ARIA attributes are added to enable it. Note the sample `show-me` id attribute used to link the label to show/hide content.
+
+### Usage
+
+#### GOVUK.ShowHideContent
+
+To apply this behaviour to elements with the above HTML pattern, call the `GOVUK.ShowHideContent` constructor:
+
+```
+var showHideContent = new GOVUK.ShowHideContent();
+showHideContent.init();
+```
+
+This will bind two event handlers to $(document.body), one for radio inputs and one for checkboxes. By listening for events bubbling up to the `body` tag, additional show/hide content added to the page will still be picked up after `.init()` is called.
+
+Alternatively, pass in your own selector. In the example below, event handlers are bound to the form instead.
+
+```
+var showHideContent = new GOVUK.ShowHideContent();
+showHideContent.init($('form.example'));
+```
diff --git a/javascripts/govuk/show-hide-content.js b/javascripts/govuk/show-hide-content.js
new file mode 100644
index 00000000..2d2e2d68
--- /dev/null
+++ b/javascripts/govuk/show-hide-content.js
@@ -0,0 +1,157 @@
+(function (global) {
+ 'use strict';
+
+ var $ = global.jQuery;
+ var GOVUK = global.GOVUK || {};
+
+ function ShowHideContent() {
+ var self = this;
+
+ // Radio and Checkbox selectors
+ var selectors = {
+ namespace: 'ShowHideContent',
+ radio: '.block-label input[type="radio"]',
+ checkbox: '.block-label input[type="checkbox"]'
+ };
+
+ // Escape name attribute for use in DOM selector
+ function escapeElementName(str) {
+ var result = str.replace('[', '\\[').replace(']', '\\]');
+ return(result);
+ }
+
+ // Adds ARIA attributes to control + associated content
+ function initToggledContent() {
+ var $control = $(this);
+ var $content = getToggledContent($control);
+
+ // Set aria-controls and defaults
+ if ($content.length) {
+ $control.attr('aria-controls', $content.attr('id'));
+ $control.attr('aria-expanded', 'false');
+ $content.attr('aria-hidden', 'true');
+ }
+ }
+
+ // Return toggled content for control
+ function getToggledContent($control) {
+ var id = $control.attr('aria-controls');
+
+ // ARIA attributes aren't set before init
+ if (!id) {
+ id = $control.parent('label').data('target');
+ }
+
+ // Find show/hide content by id
+ return $('#' + id);
+ }
+
+ // Show toggled content for control
+ function showToggledContent($control, $content) {
+
+ // Show content
+ if ($content.hasClass('js-hidden')) {
+ $content.removeClass('js-hidden');
+ $content.attr('aria-hidden', 'false');
+
+ // If the controlling input, update aria-expanded
+ if ($control.attr('aria-controls')) {
+ $control.attr('aria-expanded', 'true');
+ }
+ }
+ }
+
+ // Hide toggled content for control
+ function hideToggledContent($control, $content) {
+ $content = $content || getToggledContent($control);
+
+ // Hide content
+ if (!$content.hasClass('js-hidden')) {
+ $content.addClass('js-hidden');
+ $content.attr('aria-hidden', 'true');
+
+ // If the controlling input, update aria-expanded
+ if ($control.attr('aria-controls')) {
+ $control.attr('aria-expanded', 'false');
+ }
+ }
+ }
+
+ // Handle radio show/hide
+ function handleRadioContent($control, $content) {
+
+ // All radios in this group
+ var selector = selectors.radio + '[name=' + escapeElementName($control.attr('name')) + ']';
+ var $radios = $control.closest('form').find(selector);
+
+ // Hide radios in group
+ $radios.each(function() {
+ hideToggledContent($(this));
+ });
+
+ // Select radio button content
+ showToggledContent($control, $content);
+ }
+
+ // Handle checkbox show/hide
+ function handleCheckboxContent($control, $content) {
+
+ // Show checkbox content
+ if ($control.is(':checked')) {
+ showToggledContent($control, $content);
+ }
+
+ // Hide checkbox content
+ else {
+ hideToggledContent($control, $content);
+ }
+ }
+
+ // Set up event handlers etc
+ function init($container, selector, handler) {
+ $container = $container || $(document.body);
+
+ // Handle control clicks
+ function deferred() {
+ var $control = $(this);
+ handler($control, getToggledContent($control));
+ }
+
+ // Prepare ARIA attributes
+ var $controls = $(selector);
+ $controls.each(initToggledContent);
+
+ // Handle events
+ $container.on('click.' + selectors.namespace, selector, deferred);
+
+ // Any already :checked on init?
+ if ($controls.is(':checked')) {
+ $controls.filter(':checked').each(deferred);
+ }
+ }
+
+ // Set up radio show/hide content for container
+ self.showHideRadioToggledContent = function($container) {
+ init($container, selectors.radio, handleRadioContent);
+ };
+
+ // Set up checkbox show/hide content for container
+ self.showHideCheckboxToggledContent = function($container) {
+ init($container, selectors.checkbox, handleCheckboxContent);
+ };
+
+ // Remove event handlers
+ self.destroy = function($container) {
+ $container = $container || $(document.body);
+ $container.off('.' + selectors.namespace);
+ };
+ }
+
+ ShowHideContent.prototype.init = function($container) {
+ this.showHideRadioToggledContent($container);
+ this.showHideCheckboxToggledContent($container);
+ };
+
+ GOVUK.ShowHideContent = ShowHideContent;
+ global.GOVUK = GOVUK;
+})(window);
diff --git a/spec/manifest.js b/spec/manifest.js
index 0b7e7822..41c05c59 100644
--- a/spec/manifest.js
+++ b/spec/manifest.js
@@ -6,6 +6,7 @@ var manifest = {
'../../javascripts/govuk/modules/auto-track-event.js',
'../../javascripts/govuk/multivariate-test.js',
'../../javascripts/govuk/primary-links.js',
+ '../../javascripts/govuk/show-hide-content.js',
'../../javascripts/govuk/stick-at-top-when-scrolling.js',
'../../javascripts/govuk/stop-scrolling-at-footer.js',
'../../javascripts/govuk/selection-buttons.js',
@@ -21,6 +22,7 @@ var manifest = {
'../unit/Modules/auto-track-event.spec.js',
'../unit/multivariate-test.spec.js',
'../unit/primary-links.spec.js',
+ '../unit/show-hide-content.spec.js',
'../unit/stick-at-top-when-scrolling.spec.js',
'../unit/selection-button.spec.js',
'../unit/analytics/google-analytics-universal-tracker.spec.js',
diff --git a/spec/unit/show-hide-content.spec.js b/spec/unit/show-hide-content.spec.js
new file mode 100644
index 00000000..815a1e21
--- /dev/null
+++ b/spec/unit/show-hide-content.spec.js
@@ -0,0 +1,246 @@
+describe('show-hide-content', function() {
+ 'use strict';
+
+ beforeEach(function() {
+
+ // Sample markup
+ this.$content = $(
+
+ // Radio buttons (yes/no)
+ '' +
+
+ // Checkboxes (multiple values)
+ ''
+ );
+
+ // Find radios/checkboxes
+ var $radios = this.$content.find('input[type=radio]');
+ var $checkboxes = this.$content.find('input[type=checkbox]');
+
+ // Two radios
+ this.$radio1 = $radios.eq(0);
+ this.$radio2 = $radios.eq(1);
+
+ // Three checkboxes
+ this.$checkbox1 = $checkboxes.eq(0);
+ this.$checkbox2 = $checkboxes.eq(1);
+ this.$checkbox3 = $checkboxes.eq(2);
+
+ // Add to page
+ $(document.body).append(this.$content);
+
+ // Show/Hide content
+ this.$radioShowHide = $('#show-hide-radios');
+ this.$checkboxShowHide = $('#show-hide-checkboxes');
+
+ // Add show/hide content support
+ this.showHideContent = new GOVUK.ShowHideContent();
+ this.showHideContent.init();
+ });
+
+ afterEach(function() {
+ if (this.showHideContent) {
+ this.showHideContent.destroy();
+ }
+
+ this.$content.remove();
+ });
+
+ describe('when this.showHideContent = new GOVUK.ShowHideContent() is called', function() {
+ it('should add the aria attributes to inputs with show/hide content', function() {
+ expect(this.$radio1.attr('aria-expanded')).toBe('false');
+ expect(this.$radio1.attr('aria-controls')).toBe('show-hide-radios');
+ });
+
+ it('should add the aria attributes to show/hide content', function() {
+ expect(this.$radioShowHide.attr('aria-hidden')).toBe('true');
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should hide the show/hide content visually', function() {
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should do nothing if no radios are checked', function() {
+ expect(this.$radio1.attr('aria-expanded')).toBe('false');
+ expect(this.$radio2.attr('aria-expanded')).toBe(undefined);
+ });
+
+ it('should do nothing if no checkboxes are checked', function() {
+ expect(this.$radio1.attr('aria-expanded')).toBe('false');
+ expect(this.$radio2.attr('aria-expanded')).toBe(undefined);
+ });
+
+ describe('with non-default markup', function() {
+ beforeEach(function() {
+ this.showHideContent.destroy();
+ });
+
+ it('should do nothing if a radio without show/hide content is checked', function() {
+ this.$radio2.prop('checked', true);
+
+ // Defaults changed, initialise again
+ this.showHideContent = new GOVUK.ShowHideContent().init();
+ expect(this.$radio1.attr('aria-expanded')).toBe('false');
+ expect(this.$radioShowHide.attr('aria-hidden')).toBe('true');
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should do nothing if a checkbox without show/hide content is checked', function() {
+ this.$checkbox2.prop('checked', true);
+
+ // Defaults changed, initialise again
+ this.showHideContent = new GOVUK.ShowHideContent().init();
+ expect(this.$checkbox1.attr('aria-expanded')).toBe('false');
+ expect(this.$checkboxShowHide.attr('aria-hidden')).toBe('true');
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should do nothing if checkboxes without show/hide content is checked', function() {
+ this.$checkbox2.prop('checked', true);
+ this.$checkbox3.prop('checked', true);
+
+ // Defaults changed, initialise again
+ this.showHideContent = new GOVUK.ShowHideContent().init();
+ expect(this.$checkbox1.attr('aria-expanded')).toBe('false');
+ expect(this.$checkboxShowHide.attr('aria-hidden')).toBe('true');
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should make the show/hide content visible if its radio is checked', function() {
+ this.$radio1.prop('checked', true);
+
+ // Defaults changed, initialise again
+ this.showHideContent = new GOVUK.ShowHideContent().init();
+ expect(this.$radio1.attr('aria-expanded')).toBe('true');
+ expect(this.$radioShowHide.attr('aria-hidden')).toBe('false');
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+
+ it('should make the show/hide content visible if its checkbox is checked', function() {
+ this.$checkbox1.prop('checked', true);
+
+ // Defaults changed, initialise again
+ this.showHideContent = new GOVUK.ShowHideContent().init();
+ expect(this.$checkbox1.attr('aria-expanded')).toBe('true');
+ expect(this.$checkboxShowHide.attr('aria-hidden')).toBe('false');
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+ });
+
+ describe('and a show/hide radio receives a click', function() {
+ it('should make the show/hide content visible', function() {
+ this.$radio1.click();
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+
+ it('should add the aria attributes to show/hide content', function() {
+ this.$radio1.click();
+ expect(this.$radio1.attr('aria-expanded')).toBe('true');
+ expect(this.$radioShowHide.attr('aria-hidden')).toBe('false');
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+ });
+
+ describe('and a show/hide checkbox receives a click', function() {
+ it('should make the show/hide content visible', function() {
+ this.$checkbox1.click();
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+
+ it('should add the aria attributes to show/hide content', function() {
+ this.$checkbox1.click();
+ expect(this.$checkbox1.attr('aria-expanded')).toBe('true');
+ expect(this.$checkboxShowHide.attr('aria-hidden')).toBe('false');
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+ });
+
+ describe('and a show/hide radio receives a click, but another group radio is clicked afterwards', function() {
+ it('should make the show/hide content visible', function() {
+ this.$radio1.click();
+ this.$radio2.click();
+ expect(this.$radioShowHide.hasClass('js-hidden')).toEqual(true);
+ });
+
+ it('should add the aria attributes to show/hide content', function() {
+ this.$radio1.click();
+ this.$radio2.click();
+ expect(this.$radio1.attr('aria-expanded')).toBe('false');
+ expect(this.$radioShowHide.attr('aria-hidden')).toBe('true');
+ });
+ });
+
+ describe('and a show/hide checkbox receives a click, but another checkbox is clicked afterwards', function() {
+ it('should keep the show/hide content visible', function() {
+ this.$checkbox1.click();
+ this.$checkbox2.click();
+ expect(this.$checkboxShowHide.hasClass('js-hidden')).toEqual(false);
+ });
+
+ it('should keep the aria attributes to show/hide content', function() {
+ this.$checkbox1.click();
+ this.$checkbox2.click();
+ expect(this.$checkbox1.attr('aria-expanded')).toBe('true');
+ expect(this.$checkboxShowHide.attr('aria-hidden')).toBe('false');
+ });
+ });
+ });
+
+ describe('before this.showHideContent.destroy() is called', function() {
+
+ it('document.body should have show/hide event handlers', function() {
+ var events = $._data(document.body, 'events');
+ expect(events && events.click).toContain(jasmine.objectContaining({
+ namespace: 'ShowHideContent',
+ selector: '.block-label input[type="radio"]'
+ }));
+ expect(events && events.click).toContain(jasmine.objectContaining({
+ namespace: 'ShowHideContent',
+ selector: '.block-label input[type="checkbox"]'
+ }));
+ });
+ });
+
+ describe('when this.showHideContent.destroy() is called', function() {
+ beforeEach(function() {
+ this.showHideContent.destroy();
+ });
+
+ it('should have no show/hide event handlers', function() {
+ var events = $._data(document.body, 'events');
+ expect(events && events.click).not.toContain(jasmine.objectContaining({
+ namespace: 'ShowHideContent',
+ selector: '.block-label input[type="radio"]'
+ }));
+ expect(events && events.click).not.toContain(jasmine.objectContaining({
+ namespace: 'ShowHideContent',
+ selector: '.block-label input[type="checkbox"]'
+ }));
+ });
+ });
+});