diff --git a/addons/rose/addon/components/rose/form/fieldset/index.hbs b/addons/rose/addon/components/rose/form/fieldset/index.hbs
new file mode 100644
index 000000000..c610023dd
--- /dev/null
+++ b/addons/rose/addon/components/rose/form/fieldset/index.hbs
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/addons/rose/addon/components/rose/form/fieldset/index.js b/addons/rose/addon/components/rose/form/fieldset/index.js
new file mode 100644
index 000000000..4e2a7790e
--- /dev/null
+++ b/addons/rose/addon/components/rose/form/fieldset/index.js
@@ -0,0 +1,11 @@
+import Component from '@glimmer/component';
+import { generateComponentID } from '../../../../utilities/component-auto-id';
+
+export default class RoseFormFieldsetComponent extends Component {
+ // =attributes
+
+ /**
+ * @type {string}
+ */
+ id = generateComponentID();
+}
diff --git a/addons/rose/addon/components/rose/form/fieldset/index.stories.mdx b/addons/rose/addon/components/rose/form/fieldset/index.stories.mdx
new file mode 100644
index 000000000..9dd61a4c6
--- /dev/null
+++ b/addons/rose/addon/components/rose/form/fieldset/index.stories.mdx
@@ -0,0 +1,29 @@
+import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
+import { hbs } from 'ember-cli-htmlbars';
+import { action } from '@storybook/addon-actions';
+import { text, boolean, select } from '@storybook/addon-knobs';
+
+
+
+# Form Fieldset
+
+Renders a simple fieldset with title, description and body.
+
+```html
+
+ <:title>Title
+ <:description>Description
+ <:body>Rest of the form
+
+```
+
+
+ {{
+ template: hbs`
+
+ <:title>Title
+ <:description>Description
+ <:body>Rest of the form
+ `
+ }}
+
\ No newline at end of file
diff --git a/addons/rose/addon/styles/rose/components/form/_fieldset.scss b/addons/rose/addon/styles/rose/components/form/_fieldset.scss
new file mode 100644
index 000000000..8696ad562
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/form/_fieldset.scss
@@ -0,0 +1,14 @@
+@use '../../variables/sizing';
+@use '../../utilities/type';
+
+.rose-form-fieldset {
+ .rose-form-fieldset-title {
+ @include type.type(l, bold);
+ }
+
+ .rose-form-fieldset-description {
+ margin: sizing.rems(xxs) 0;
+ @include type.type(xs);
+ color: var(--ui-gray);
+ }
+}
diff --git a/addons/rose/addon/styles/rose/components/form/_index.scss b/addons/rose/addon/styles/rose/components/form/_index.scss
index 815a1aaf5..73137ac8d 100644
--- a/addons/rose/addon/styles/rose/components/form/_index.scss
+++ b/addons/rose/addon/styles/rose/components/form/_index.scss
@@ -5,3 +5,4 @@
@import 'checkbox';
@import 'select';
@import 'radio';
+@import 'fieldset';
diff --git a/addons/rose/app/components/rose/form/fieldset.js b/addons/rose/app/components/rose/form/fieldset.js
new file mode 100644
index 000000000..9619ad95c
--- /dev/null
+++ b/addons/rose/app/components/rose/form/fieldset.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/form/fieldset';
diff --git a/addons/rose/tests/integration/components/rose/form/fieldset-test.js b/addons/rose/tests/integration/components/rose/form/fieldset-test.js
new file mode 100644
index 000000000..a75efce1a
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/form/fieldset-test.js
@@ -0,0 +1,89 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/form/fieldset', function (hooks) {
+ setupRenderingTest(hooks);
+
+ // Global values for mocking
+ this.mockTitle = 'Title';
+ this.mockDescription = 'Description';
+ this.mockBody = 'Rest of the form';
+
+ // Wrap the rendering in a function to avoid repeat code
+ async function renderComponent(noDescription) {
+ if (noDescription === true) {
+ return render(hbs`
+
+ <:title>{{this.mockTitle}}
+ <:body>{{this.mockBody}}
+
+ `);
+ }
+
+ return render(hbs`
+
+ <:title>{{this.mockTitle}}
+ <:description>{{this.mockDescription}}
+ <:body>{{this.mockBody}}
+
+ `);
+ }
+
+ test('it renders the full component', async function (assert) {
+ assert.expect(4);
+ await renderComponent(false);
+ assert.ok(find('.rose-form-fieldset'));
+ assert.equal(
+ find('.rose-form-fieldset-title').textContent.trim(),
+ this.mockTitle
+ );
+ assert.equal(
+ find('.rose-form-fieldset-description').textContent.trim(),
+ this.mockDescription
+ );
+ assert.equal(
+ find('.rose-form-fieldset-body').textContent.trim(),
+ this.mockBody
+ );
+ });
+
+ test('it renders with no description', async function (assert) {
+ assert.expect(4);
+ await renderComponent(true);
+ assert.ok(find('.rose-form-fieldset'));
+ assert.equal(
+ find('.rose-form-fieldset-title').textContent.trim(),
+ this.mockTitle
+ );
+ assert.notOk(find('.rose-form-fieldset-description'));
+ assert.equal(
+ find('.rose-form-fieldset-body').textContent.trim(),
+ this.mockBody
+ );
+ });
+
+ test('it supports aria description for fieldset with description provided', async function (assert) {
+ assert.expect(1);
+ await renderComponent(false);
+ const descriptionElement = find('.rose-form-fieldset-description');
+ const fieldsetElement = find('.rose-form-fieldset');
+
+ assert.equal(
+ fieldsetElement.getAttribute('aria-describedby'),
+ descriptionElement.id,
+ 'Fieldset is described by description'
+ );
+ });
+
+ test('it does not support aria description for fieldset with no description provided', async function (assert) {
+ assert.expect(3);
+ await renderComponent(true);
+ const fieldsetElement = find('.rose-form-fieldset');
+
+ assert.ok(find('.rose-form-fieldset'));
+ assert.notOk(find('.rose-form-fieldset-description'));
+ assert.notOk(fieldsetElement.getAttribute('aria-describedby'));
+ });
+});