diff --git a/src/event/FieldVisibilityEventHandler.js b/src/event/FieldVisibilityEventHandler.js
new file mode 100644
index 0000000..aa5b33d
--- /dev/null
+++ b/src/event/FieldVisibilityEventHandler.js
@@ -0,0 +1,111 @@
+/**
+ * Field visibility event handler
+ *
+ * Copyright (C) 2017 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+const Class = require( 'easejs' ).Class;
+const EventHandler = require( './EventHandler' );
+const UnknownEventError = require( './UnknownEventError' );
+
+
+/**
+ * Shows/hides fields according to event id
+ *
+ * @todo use something more appropriate than Ui
+ * @todo should not be concerned with data validators
+ */
+module.exports = Class( 'FieldVisibilityEventHandler' )
+ .implement( EventHandler )
+ .extend(
+{
+ /**
+ * Client UI
+ * @type {Ui}
+ */
+ 'private _ui': null,
+
+ /**
+ * Field data validator
+ * @type {DataValidator}
+ */
+ 'private _data_validator': null,
+
+
+ /**
+ * Initialize with Client UI
+ *
+ * @param {Ui} stepui Client UI
+ * @param {DataValidator} data_validator field data validator
+ */
+ __construct( stepui, data_validator )
+ {
+ this._ui = stepui;
+ this._data_validator = data_validator;
+ },
+
+
+ /**
+ * Show/hide specified fields
+ *
+ * If a given field is not known then it will be silently ignored; the
+ * callback `callback` will still be invoked.
+ *
+ * This relies on a poorly designed API that should change in the future.
+ *
+ * @param {string} event_id event id
+ * @param {function(*,Object)} callback continuation to invoke on completion
+ *
+ * @param {elementName:string, indexes:Array.} data
+ *
+ * @return {EventHandler} self
+ */
+ 'public handle'( event_id, callback, { elementName: field_name, indexes } )
+ {
+ // TODO: Law of Demeter!
+ const group = this._ui.getCurrentStep()
+ .getElementGroup( field_name );
+
+ // we probably should care, but we don't right now
+ if ( !group )
+ {
+ callback();
+ return;
+ }
+
+ const action = ( () =>
+ {
+ switch ( event_id )
+ {
+ case 'show':
+ return group.showField.bind( group );
+
+ case 'hide':
+ return group.hideField.bind( group );
+
+ default:
+ throw UnknownEventError( `Unknown visibility event: ${event_id}` );
+ }
+ } )();
+
+ this._data_validator.clearFailures( [ field_name ] );
+ indexes.forEach( field_i => action( field_name, field_i ) );
+
+ callback();
+ }
+} );
diff --git a/test/event/FieldVisibilityEventHandlerTest.js b/test/event/FieldVisibilityEventHandlerTest.js
new file mode 100644
index 0000000..005e0e7
--- /dev/null
+++ b/test/event/FieldVisibilityEventHandlerTest.js
@@ -0,0 +1,145 @@
+/**
+ * Test case for FieldVisibilityEventHandler
+ *
+ * Copyright (C) 2017 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+const event = require( '../../' ).event;
+const expect = require( 'chai' ).expect;
+const Class = require( 'easejs' ).Class;
+
+const {
+ FieldVisibilityEventHandler: Sut,
+ UnknownEventError
+} = event;
+
+
+describe( 'FieldVisibilityEventHandler', () =>
+{
+ it( 'shows/hides each element index', done =>
+ {
+ const name = 'field_name';
+ const shown = { [name]: [] };
+ const hidden = { [name]: [] };
+
+ const sut = Sut(
+ createMockStepUi(
+ name,
+ ( field, index ) => shown[ field ].push( index ),
+ ( field, index ) => hidden[ field ].push( index )
+ ),
+ createStubDataProvider()
+ );
+
+ // purposefully sparse indexes
+ const show_indexes = [ 2, 4, ];
+ const hide_indexes = [ 0, 3, ];
+
+ const show_data = {
+ elementName: name,
+ indexes: show_indexes,
+ };
+
+ const hide_data = {
+ elementName: name,
+ indexes: hide_indexes,
+ };
+
+ sut.handle( 'show', () =>
+ {
+ // implicitly ensures proper name is passed
+ expect( shown[ name ] ).to.deep.equal( show_indexes );
+
+ sut.handle( 'hide', () =>
+ {
+ expect( hidden[ name ] ).to.deep.equal( hide_indexes );
+ done();
+ }, hide_data );
+ }, show_data );
+ } );
+
+
+ it( 'throws error given unknown event', () =>
+ {
+ expect( () =>
+ {
+ Sut( createMockStepUi() ).handle( 'unknown', () => {}, {} );
+ } ).to.throw( UnknownEventError );
+ } );
+
+
+ it( 'ignores unknown groups', done =>
+ {
+ expect( () =>
+ {
+ Sut( {
+ getCurrentStep: () => ( { getElementGroup: () => null } )
+ } ).handle( 'hide', done, {} )
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'clears failures on hidden fields', done =>
+ {
+ const name = 'foo_bar';
+
+ const hide_data = {
+ elementName: name,
+ indexes: [ 0 ],
+ };
+
+ Sut(
+ createMockStepUi( name, () => {}, () => {} ),
+ createStubDataProvider( failures =>
+ {
+ expect( failures )
+ .to.deep.equal( [ name ] )
+
+ // we don't care about the rest of the processing at this
+ // point
+ done();
+ } )
+ ).handle( 'hide', () => {}, hide_data );
+ } );
+} );
+
+
+function createMockStepUi( expected_name, showf, hidef )
+{
+ return {
+ getCurrentStep: () => ( {
+ getElementGroup( field_name )
+ {
+ expect( field_name ).to.equal( expected_name );
+
+ return {
+ showField: showf,
+ hideField: hidef,
+ };
+ }
+ } ),
+ };
+}
+
+
+function createStubDataProvider( fail_callback )
+{
+ return {
+ clearFailures: fail_callback || () => {},
+ };
+}