diff --git a/ui/app/components/toggle.js b/ui/app/components/toggle.js
new file mode 100644
index 0000000000..2732798dd2
--- /dev/null
+++ b/ui/app/components/toggle.js
@@ -0,0 +1,40 @@
+/**
+ * @module Toggle
+ * Toggle components are used to indicate boolean values which can be toggled on or off.
+ * They are a stylistic alternative to checkboxes, but still use the input[type="checkbox"] under the hood.
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {function} onChange - onChange is triggered on checkbox change (select, deselect). Must manually mutate checked value
+ * @param {string} name - name is passed along to the form field, as well as to generate the ID of the input & "for" value of the label
+ * @param {boolean} [checked=false] - checked status of the input, and must be passed in and mutated from the parent
+ * @param {boolean} [disabled=false] - disabled makes the switch unclickable
+ * @param {string} [size='medium'] - Sizing can be small or medium
+ * @param {string} [status='normal'] - Status can be normal or success, which makes the switch have a blue background when checked=true
+ */
+
+import Component from '@ember/component';
+import { computed } from '@ember/object';
+
+export default Component.extend({
+ tagName: '',
+ checked: false,
+ disabled: false,
+ size: 'normal',
+ status: 'normal',
+ safeId: computed('name', function() {
+ return `toggle-${this.name}`;
+ }),
+ inputClasses: computed('size', 'status', function() {
+ const sizeClass = `is-${this.size}`;
+ const statusClass = `is-${this.status}`;
+ return `toggle ${statusClass} ${sizeClass}`;
+ }),
+ actions: {
+ handleChange(value) {
+ this.onChange(value);
+ },
+ },
+});
diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss
index e265d4cd7a..03ffa0f88c 100644
--- a/ui/app/styles/core.scss
+++ b/ui/app/styles/core.scss
@@ -35,6 +35,7 @@
@import './core/tables';
@import './core/tags';
@import './core/title';
+@import './core/toggle';
// bulma additions
@import './core/layout';
diff --git a/ui/app/styles/core/toggle.scss b/ui/app/styles/core/toggle.scss
new file mode 100644
index 0000000000..869da9e128
--- /dev/null
+++ b/ui/app/styles/core/toggle.scss
@@ -0,0 +1,104 @@
+/* COPIED FROM BULMA/SWITCH */
+.toggle[type='checkbox'] {
+ outline: 0;
+ user-select: none;
+ position: absolute;
+ margin-bottom: 0;
+ opacity: 0;
+ left: 0;
+}
+.toggle[type='checkbox'][disabled] {
+ cursor: not-allowed;
+}
+.toggle[type='checkbox'][disabled] + label {
+ opacity: 0.5;
+}
+.toggle[type='checkbox'][disabled] + label::before {
+ opacity: 0.5;
+}
+.toggle[type='checkbox'][disabled] + label::after {
+ opacity: 0.5;
+}
+.toggle[type='checkbox'][disabled] + label:hover {
+ cursor: not-allowed;
+}
+.toggle[type='checkbox'] + label {
+ position: relative;
+ display: inline-block;
+ font-size: 1rem;
+ padding-left: 3.5rem;
+ cursor: pointer;
+}
+.toggle[type='checkbox'] + label::before {
+ position: absolute;
+ display: block;
+ top: 0;
+ left: 0;
+ width: 3rem;
+ height: 1.5rem;
+ border: 0.1rem solid transparent;
+ border-radius: 0.75rem;
+ background: $ui-gray-300;
+ content: '';
+}
+.toggle[type='checkbox'] + label::after {
+ display: block;
+ position: absolute;
+ top: 0.25rem;
+ left: 0.25rem;
+ width: 1rem;
+ height: 1rem;
+ transform: translate3d(0, 0, 0);
+ border-radius: 50%;
+ background: white;
+ transition: all 0.25s ease-out;
+ content: '';
+}
+.toggle[type='checkbox']:checked + label::before {
+ background: $ui-gray-700;
+}
+.toggle[type='checkbox']:checked + label::after {
+ left: 1.625rem;
+}
+
+/* CUSTOM */
+.toggle[type='checkbox'] {
+ &.is-small {
+ + label {
+ font-size: $size-8;
+ font-weight: bold;
+ padding-left: $size-8 * 2.5;
+ margin: 0 0.25rem;
+ &::before {
+ top: $size-8 / 5;
+ height: $size-8;
+ width: $size-8 * 2;
+ }
+ &::after {
+ width: $size-8 * 0.8;
+ height: $size-8 * 0.8;
+ transform: translateX(0.15rem);
+ left: 0;
+ top: $size-8/ 4;
+ }
+ }
+ &:checked + label::after {
+ left: 0;
+ transform: translateX(($size-8 * 2) - ($size-8 * 0.94));
+ }
+ }
+
+ &.is-large {
+ width: 4.5rem;
+ height: 2.25rem;
+ }
+}
+.toggle[type='checkbox'].is-small + label::after {
+ will-change: left;
+}
+.toggle[type='checkbox']:focus + label {
+ box-shadow: 0 0 1px $blue;
+}
+.toggle[type='checkbox'].is-success:checked + label::before {
+ background: $blue;
+}
diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs
index 06bf6bdf3b..9719fb0334 100644
--- a/ui/app/templates/components/secret-edit.hbs
+++ b/ui/app/templates/components/secret-edit.hbs
@@ -26,17 +26,16 @@
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
-
-
+
+ JSON
+
{{/unless}}
diff --git a/ui/app/templates/components/toggle.hbs b/ui/app/templates/components/toggle.hbs
new file mode 100644
index 0000000000..16cf4700be
--- /dev/null
+++ b/ui/app/templates/components/toggle.hbs
@@ -0,0 +1,11 @@
+{{input
+ id=safeId
+ name=name
+ type="checkbox"
+ checked=checked
+ change=(action "handleChange" value='target.checked')
+ class=inputClasses
+ disabled=disabled
+ data-test-toggle-input=name
+}}
+
\ No newline at end of file
diff --git a/ui/stories/toggle.md b/ui/stories/toggle.md
new file mode 100644
index 0000000000..148a11b3da
--- /dev/null
+++ b/ui/stories/toggle.md
@@ -0,0 +1,29 @@
+
+
+## Toggle
+Toggle components are used to indicate boolean values which can be toggled on or off.
+They are a stylistic alternative to checkboxes, but still use the input[type=checkbox] under the hood.
+
+**Params**
+
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| onChange | function | | onChange is triggered on checkbox change (select, deselect). Must manually mutate checked value |
+| name | string | | name is passed along to the form field, as well as to generate the ID of the input & "for" value of the label |
+| [checked] | boolean | false | checked status of the input, and must be passed in and mutated from the parent |
+| [disabled] | boolean | false | disabled makes the switch unclickable |
+| [size] | string | "'medium'" | Sizing can be small or medium |
+| [status] | string | "'normal'" | Status can be normal or success, which makes the switch have a blue background when checked=true |
+
+**Example**
+
+```js
+
+```
+
+**See**
+
+- [Uses of Toggle](https://github.com/hashicorp/vault/search?l=Handlebars&q=Toggle+OR+toggle)
+- [Toggle Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/toggle.js)
+
+---
diff --git a/ui/stories/toggle.stories.js b/ui/stories/toggle.stories.js
new file mode 100644
index 0000000000..c08e1613c7
--- /dev/null
+++ b/ui/stories/toggle.stories.js
@@ -0,0 +1,40 @@
+import hbs from 'htmlbars-inline-precompile';
+import { storiesOf } from '@storybook/ember';
+import notes from './toggle.md';
+import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
+
+storiesOf('Toggle/', module)
+ .addParameters({ options: { showPanel: true } })
+ .addDecorator(withKnobs())
+ .add(
+ `Toggle`,
+ () => ({
+ template: hbs`
+ Toggle
+
+ {{yielded}}
+
+ `,
+ context: {
+ name: text('name', 'my-checkbox'),
+ checked: boolean('checked', true),
+ yielded: text('yield', 'Label content here ✔️'),
+ onChange() {
+ this.set('checked', !this.checked);
+ },
+ disabled: boolean('disabled', false),
+ size: select('size', ['small', 'medium'], 'small'),
+ status: select('status', ['normal', 'success'], 'success'),
+ },
+ }),
+ { notes }
+ );
diff --git a/ui/tests/integration/components/secret-edit-test.js b/ui/tests/integration/components/secret-edit-test.js
index 6ffa24a851..3f92b6b9b3 100644
--- a/ui/tests/integration/components/secret-edit-test.js
+++ b/ui/tests/integration/components/secret-edit-test.js
@@ -35,7 +35,7 @@ module('Integration | Component | secret edit', function(hooks) {
});
await render(hbs`{{secret-edit mode=mode model=model }}`);
- assert.dom('[data-test-secret-json-toggle]').isDisabled();
+ assert.dom('[data-test-toggle-input="json"]').isDisabled();
});
test('it does JSON toggle in show mode when showing string data', async function(assert) {
@@ -49,7 +49,7 @@ module('Integration | Component | secret edit', function(hooks) {
});
await render(hbs`{{secret-edit mode=mode model=model }}`);
- assert.dom('[data-test-secret-json-toggle]').isNotDisabled();
+ assert.dom('[data-test-toggle-input="json"]').isNotDisabled();
});
test('it shows an error when creating and data is not an object', async function(assert) {
diff --git a/ui/tests/integration/components/toggle-test.js b/ui/tests/integration/components/toggle-test.js
new file mode 100644
index 0000000000..06b27cc664
--- /dev/null
+++ b/ui/tests/integration/components/toggle-test.js
@@ -0,0 +1,50 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, findAll } from '@ember/test-helpers';
+import sinon from 'sinon';
+import hbs from 'htmlbars-inline-precompile';
+
+let handler = (data, e) => {
+ if (e && e.preventDefault) e.preventDefault();
+ return;
+};
+
+module('Integration | Component | toggle', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ this.set('handler', sinon.spy(handler));
+
+ await render(hbs``);
+
+ assert.equal(findAll('label')[0].textContent.trim(), '');
+
+ await render(hbs`
+
+ template block text
+
+ `);
+ assert.dom('[data-test-toggle-label="thing"]').exists('toggle label exists');
+ assert.equal(find('#test-value').textContent.trim(), 'template block text', 'yielded text renders');
+ });
+
+ test('it has the correct classes', async function(assert) {
+ this.set('handler', sinon.spy(handler));
+ await render(hbs`
+
+ template block text
+
+ `);
+ assert.dom('.toggle.is-small').exists('toggle has is-small class');
+ });
+});
diff --git a/ui/tests/pages/secrets/backend/kv/edit-secret.js b/ui/tests/pages/secrets/backend/kv/edit-secret.js
index cdb7007e6f..3a7a719117 100644
--- a/ui/tests/pages/secrets/backend/kv/edit-secret.js
+++ b/ui/tests/pages/secrets/backend/kv/edit-secret.js
@@ -11,7 +11,7 @@ export default create({
confirmBtn: clickable('[data-test-confirm-button]'),
visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
- toggleJSON: clickable('[data-test-secret-json-toggle]'),
+ toggleJSON: clickable('[data-test-toggle-input="json"]'),
hasMetadataFields: isPresent('[data-test-metadata-fields]'),
showsNoCASWarning: isPresent('[data-test-v2-no-cas-warning]'),
showsV2WriteWarning: isPresent('[data-test-v2-write-without-read]'),