diff --git a/addon/components/o-s-s/empty-state.hbs b/addon/components/o-s-s/empty-state.hbs
new file mode 100644
index 00000000..cdb90ed7
--- /dev/null
+++ b/addon/components/o-s-s/empty-state.hbs
@@ -0,0 +1,18 @@
+
+ {{#if (has-block "image")}}
+ {{yield to="image"}}
+ {{else if @badgeIcon}}
+
+ {{/if}}
+
+
+ {{@title}}
+
+
+ {{@subtitle}}
+
+
+ {{#if (has-block "actions")}}
+ {{yield to="actions"}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/addon/components/o-s-s/empty-state.stories.js b/addon/components/o-s-s/empty-state.stories.js
new file mode 100644
index 00000000..5277e950
--- /dev/null
+++ b/addon/components/o-s-s/empty-state.stories.js
@@ -0,0 +1,95 @@
+import { hbs } from 'ember-cli-htmlbars';
+
+export default {
+ title: 'Components/OSS::EmptyState',
+ component: 'empty state',
+ argTypes: {
+ badgeIcon: {
+ description: 'A font-awesome icon to be displayed in a badge',
+ table: {
+ type: {
+ summary: 'string'
+ },
+ defaultValue: { summary: 'undefined' }
+ },
+ control: { type: 'text' }
+ },
+ title: {
+ description: 'A title displayed below the icon or badge in the component',
+ table: {
+ type: {
+ summary: 'string'
+ },
+ defaultValue: { summary: 'undefined' }
+ },
+ control: { type: 'text' }
+ },
+ subtitle: {
+ description: 'A subtitle displayed under the title in the component',
+ table: {
+ type: {
+ summary: 'string'
+ },
+ defaultValue: { summary: 'undefined' }
+ },
+ control: { type: 'text' }
+ },
+ size: {
+ description: 'The size of the state',
+ table: {
+ type: {
+ summary: 'string'
+ },
+ defaultValue: { summary: 'undefined' }
+ },
+ control: { type: 'select' },
+ options: ['sm', 'md']
+ }
+ },
+ parameters: {
+ docs: {
+ description: {
+ component: 'A component used when there is nothing to display on a page'
+ }
+ }
+ }
+};
+
+const defaultArgs = {
+ badgeIcon: 'fa-thumbs-up',
+ title: 'Empty State Title',
+ subtitle: 'Additional information here',
+ size: 'md'
+};
+
+const Template = (args) => ({
+ template: hbs``,
+ context: args
+});
+
+const ImageTemplate = (args) => ({
+ template: hbs`
+ <:image>
+
+
+ `,
+ context: args
+});
+
+const ActionTemplate = (args) => ({
+ template: hbs`
+ <:actions>
+
+
+ `,
+ context: args
+});
+
+export const Default = Template.bind({});
+Default.args = defaultArgs;
+
+export const UsageWithImage = ImageTemplate.bind({});
+UsageWithImage.args = defaultArgs;
+
+export const UsageWithActions = ActionTemplate.bind({});
+UsageWithActions.args = defaultArgs;
diff --git a/addon/components/o-s-s/empty-state.ts b/addon/components/o-s-s/empty-state.ts
new file mode 100644
index 00000000..59016f01
--- /dev/null
+++ b/addon/components/o-s-s/empty-state.ts
@@ -0,0 +1,29 @@
+import type { htmlSafe } from '@ember/template';
+import { assert } from '@ember/debug';
+import Component from '@glimmer/component';
+
+interface OSSEmptyStateComponentSignature {
+ badgeIcon?: string;
+ title: ReturnType;
+ subtitle: ReturnType;
+ size?: 'sm' | 'md';
+}
+
+const ALLOWED_SIZES: string[] = ['sm', 'md'];
+
+export default class OSSEmptyStateComponent extends Component {
+ constructor(owner: unknown, args: OSSEmptyStateComponentSignature) {
+ super(owner, args);
+
+ assert('[component][OSS::EmptyState] The title parameter is mandatory', typeof args.title === 'string');
+ assert('[component][OSS::EmptyState] The subtitle parameter is mandatory', typeof args.subtitle === 'string');
+ }
+
+ get titleSize(): string {
+ return this.size === 'sm' ? 'md' : 'lg';
+ }
+
+ get size(): string {
+ return this.args.size && ALLOWED_SIZES.includes(this.args.size) ? this.args.size : 'md';
+ }
+}
diff --git a/app/components/o-s-s/empty-state.js b/app/components/o-s-s/empty-state.js
new file mode 100644
index 00000000..7f68cc78
--- /dev/null
+++ b/app/components/o-s-s/empty-state.js
@@ -0,0 +1 @@
+export { default } from '@upfluence/oss-components/components/o-s-s/empty-state';
diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs
index c699a333..09c48316 100644
--- a/tests/dummy/app/templates/index.hbs
+++ b/tests/dummy/app/templates/index.hbs
@@ -276,4 +276,44 @@
+
+
+ Empty state
+
+
+
+ <:image>
+
+
+ <:actions>
+
+
+
+
+
+ <:actions>
+
+
+
+
+
+ <:image>
+
+
+ <:actions>
+
+
+
+
+
+ <:actions>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration/components/o-s-s/empty-state-test.ts b/tests/integration/components/o-s-s/empty-state-test.ts
new file mode 100644
index 00000000..d0a5e201
--- /dev/null
+++ b/tests/integration/components/o-s-s/empty-state-test.ts
@@ -0,0 +1,96 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, setupOnerror } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | o-s-s/empty-state', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders with default properties', async function (assert) {
+ await render(hbs``);
+
+ assert.dom('div.font-color-gray-900').hasText('No Data');
+ assert.dom('div.font-color-gray-500').hasText('Try again later');
+ });
+
+ test('it renders with a badge icon', async function (assert) {
+ this.set('image', 'fa-thumbs-up');
+ await render(hbs``);
+
+ assert.dom('div.font-color-gray-900').hasText('No Data');
+ assert.dom('div.font-color-gray-500').hasText('Try again later');
+ assert.dom('.upf-badge').exists();
+ });
+
+ test('it supports named-block usage for image', async function (assert) {
+ await render(hbs`
+
+ <:image>
+
+
+
+ `);
+
+ assert.dom('div.font-color-gray-900').hasText('No Data');
+ assert.dom('div.font-color-gray-500').hasText('Try again later');
+ assert.dom('img').exists();
+ });
+
+ test('it supports named-block usage for actions', async function (assert) {
+ await render(hbs`
+
+ <:actions>
+
+
+
+ `);
+
+ assert.dom('div.font-color-gray-900').hasText('No Data');
+ assert.dom('div.font-color-gray-500').hasText('Try again later');
+ assert.dom('button').hasText('Retry');
+ });
+
+ module('component size', function (hooks) {
+ test('it applies md sizes by default', async function (assert) {
+ await render(hbs``);
+ assert.dom('div.font-color-gray-900').hasClass('font-size-lg');
+ assert.dom('div.font-color-gray-500').hasClass('font-size-md');
+ });
+
+ test('it applies md sizes when given a wrong size', async function (assert) {
+ await render(hbs``);
+ assert.dom('div.font-color-gray-900').hasClass('font-size-lg');
+ assert.dom('div.font-color-gray-500').hasClass('font-size-md');
+ });
+
+ test('it applies md sizes when specified', async function (assert) {
+ await render(hbs``);
+ assert.dom('div.font-color-gray-900').hasClass('font-size-lg');
+ assert.dom('div.font-color-gray-500').hasClass('font-size-md');
+ });
+
+ test('it applies sm sizes when specified', async function (assert) {
+ await render(hbs``);
+ assert.dom('div.font-color-gray-900').hasClass('font-size-md');
+ assert.dom('div.font-color-gray-500').hasClass('font-size-sm');
+ });
+ });
+
+ module('error management', function (hooks) {
+ test('it throws an error if the @title parameter is not passed', async function (assert) {
+ setupOnerror((err: any) => {
+ assert.equal(err.message, 'Assertion Failed: [component][OSS::EmptyState] The title parameter is mandatory');
+ });
+
+ await render(hbs``);
+ });
+
+ test('it throws an error if the @subtitle parameter is not passed', async function (assert) {
+ setupOnerror((err: any) => {
+ assert.equal(err.message, 'Assertion Failed: [component][OSS::EmptyState] The subtitle parameter is mandatory');
+ });
+
+ await render(hbs``);
+ });
+ });
+});