diff --git a/ui-v2/app/components/state/README.mdx b/ui-v2/app/components/state/README.mdx
new file mode 100644
index 0000000000..8a59e4df11
--- /dev/null
+++ b/ui-v2/app/components/state/README.mdx
@@ -0,0 +1,38 @@
+## State
+
+`Currently Idle`
+
+`` is a renderless component that eases rendering of different states
+from within templates. State objects could be manually made state objects and
+xstate state objects. It's very similar to a normal conditional in that if the
+state identifier matches the current state, the contents of the component will
+be shown.
+
+### Arguments
+
+| Argument/Attribute | Type | Default | Description |
+| --- | --- | --- | --- |
+| `state` | `object` | | An object that implements a `match` method |
+| `matches` | `String\|Array` | | A state identifier (or array of state identifiers) to match on |
+
+
+### Example
+
+```handlebars
+
+ Currently Idle
+
+
+ Currently Loading
+
+
+ Idle and loading
+
+```
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/state/index.hbs b/ui-v2/app/components/state/index.hbs
new file mode 100644
index 0000000000..a848649944
--- /dev/null
+++ b/ui-v2/app/components/state/index.hbs
@@ -0,0 +1,3 @@
+{{#if rendering}}
+ {{yield}}
+{{/if}}
\ No newline at end of file
diff --git a/ui-v2/app/components/state/index.js b/ui-v2/app/components/state/index.js
new file mode 100644
index 0000000000..117863d93d
--- /dev/null
+++ b/ui-v2/app/components/state/index.js
@@ -0,0 +1,18 @@
+import Component from '@ember/component';
+import { set } from '@ember/object';
+import { inject as service } from '@ember/service';
+
+export default Component.extend({
+ service: service('state'),
+ tagName: '',
+ didReceiveAttrs: function() {
+ if (typeof this.state === 'undefined') {
+ return;
+ }
+ let match = true;
+ if (typeof this.matches !== 'undefined') {
+ match = this.service.matches(this.state, this.matches);
+ }
+ set(this, 'rendering', match);
+ },
+});
diff --git a/ui-v2/app/helpers/state-matches.js b/ui-v2/app/helpers/state-matches.js
new file mode 100644
index 0000000000..354898fccd
--- /dev/null
+++ b/ui-v2/app/helpers/state-matches.js
@@ -0,0 +1,9 @@
+import Helper from '@ember/component/helper';
+import { inject as service } from '@ember/service';
+
+export default Helper.extend({
+ state: service('state'),
+ compute([state, values], hash) {
+ return this.state.matches(state, values);
+ },
+});
diff --git a/ui-v2/app/services/state.js b/ui-v2/app/services/state.js
new file mode 100644
index 0000000000..deeb3dacbf
--- /dev/null
+++ b/ui-v2/app/services/state.js
@@ -0,0 +1,14 @@
+import Service from '@ember/service';
+export default Service.extend({
+ matches: function(state, matches) {
+ const values = Array.isArray(matches) ? matches : [matches];
+ return values.some(item => {
+ return state.matches(item);
+ });
+ },
+ state: function(cb) {
+ return {
+ matches: cb,
+ };
+ },
+});
diff --git a/ui-v2/tests/integration/components/state-test.js b/ui-v2/tests/integration/components/state-test.js
new file mode 100644
index 0000000000..814b02cbcb
--- /dev/null
+++ b/ui-v2/tests/integration/components/state-test.js
@@ -0,0 +1,33 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | state', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ this.set('state', {
+ matches: function(id) {
+ return id === 'idle';
+ },
+ });
+ await render(hbs`
+
+ Currently Idle
+
+ `);
+
+ assert.equal(this.element.textContent.trim(), 'Currently Idle');
+ await render(hbs`
+
+ Currently Idle
+
+ `);
+
+ assert.equal(this.element.textContent.trim(), '');
+ });
+});
diff --git a/ui-v2/tests/integration/helpers/state-matches-test.js b/ui-v2/tests/integration/helpers/state-matches-test.js
new file mode 100644
index 0000000000..c9bbdd058a
--- /dev/null
+++ b/ui-v2/tests/integration/helpers/state-matches-test.js
@@ -0,0 +1,32 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Helper | state-matches', function(hooks) {
+ setupRenderingTest(hooks);
+
+ // Replace this with your real tests.
+ test('it returns true/false when the state or state in an array matches', async function(assert) {
+ this.set('state', {
+ matches: function(id) {
+ return id === 'idle';
+ },
+ });
+
+ await render(hbs`{{state-matches state 'idle'}}`);
+ assert.equal(this.element.textContent.trim(), 'true');
+
+ await render(hbs`{{state-matches state 'loading'}}`);
+ assert.equal(this.element.textContent.trim(), 'false');
+
+ await render(hbs`{{state-matches state (array 'idle' 'loading')}}`);
+ assert.equal(this.element.textContent.trim(), 'true');
+
+ await render(hbs`{{state-matches state (array 'loading' 'idle')}}`);
+ assert.equal(this.element.textContent.trim(), 'true');
+
+ await render(hbs`{{state-matches state (array 'loading' 'deleting')}}`);
+ assert.equal(this.element.textContent.trim(), 'false');
+ });
+});
diff --git a/ui-v2/tests/unit/services/state-test.js b/ui-v2/tests/unit/services/state-test.js
new file mode 100644
index 0000000000..70dd5e0998
--- /dev/null
+++ b/ui-v2/tests/unit/services/state-test.js
@@ -0,0 +1,27 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Service | state', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('.state creates a state matchable object', function(assert) {
+ const service = this.owner.lookup('service:state');
+ const actual = service.state(id => id === 'idle');
+ assert.equal(typeof actual, 'object');
+ assert.equal(typeof actual.matches, 'function');
+ });
+ test('.matches performs a match correctly', function(assert) {
+ const service = this.owner.lookup('service:state');
+ const state = service.state(id => id === 'idle');
+ assert.equal(service.matches(state, 'idle'), true);
+ assert.equal(service.matches(state, 'loading'), false);
+ });
+ test('.matches performs a match correctly when passed an array', function(assert) {
+ const service = this.owner.lookup('service:state');
+ const state = service.state(id => id === 'idle');
+ assert.equal(service.matches(state, ['idle']), true);
+ assert.equal(service.matches(state, ['loading', 'idle']), true);
+ assert.equal(service.matches(state, ['loading', 'deleting']), false);
+ });
+});