diff --git a/lang/en/fieldtypes.php b/lang/en/fieldtypes.php
index 7a3c9e0d7e8..3b75a19f65e 100644
--- a/lang/en/fieldtypes.php
+++ b/lang/en/fieldtypes.php
@@ -117,6 +117,10 @@
'icon.config.mode' => 'Choose a dropdown or compact picker.',
'icon.config.set' => 'Default CP icons, or a set registered with `Icons::register()`.',
'icon.title' => 'Icon',
+ 'info.config.content' => 'The information to display. Markdown is supported, including links and lists.',
+ 'info.config.icon' => 'Choose an icon, or leave empty to use the state\'s default icon.',
+ 'info.config.state' => 'Choose the visual importance of the information.',
+ 'info.title' => 'Info',
'integer.config.max' => 'The maximum allowed value.',
'integer.config.min' => 'The minimum allowed value.',
'integer.config.step' => 'The interval between valid numbers.',
diff --git a/resources/js/bootstrap/fieldtypes.js b/resources/js/bootstrap/fieldtypes.js
index 5cb99a0c35e..414809b79b7 100644
--- a/resources/js/bootstrap/fieldtypes.js
+++ b/resources/js/bootstrap/fieldtypes.js
@@ -34,6 +34,7 @@ import GroupFieldtype from '../components/fieldtypes/GroupFieldtype.vue';
import HiddenFieldtype from '../components/fieldtypes/HiddenFieldtype.vue';
import HtmlFieldtype from '../components/fieldtypes/HtmlFieldtype.vue';
import IconFieldtype from '../components/fieldtypes/IconFieldtype.vue';
+import InfoFieldtype from '../components/fieldtypes/InfoFieldtype.vue';
import IntegerFieldtype from '../components/fieldtypes/IntegerFieldtype.vue';
import LinkFieldtype from '../components/fieldtypes/LinkFieldtype.vue';
import LinkIndexFieldtype from '../components/fieldtypes/LinkIndexFieldtype.vue';
@@ -108,6 +109,7 @@ export default function registerFieldtypes(app) {
app.component('hidden-fieldtype', HiddenFieldtype);
app.component('html-fieldtype', HtmlFieldtype);
app.component('icon-fieldtype', IconFieldtype);
+ app.component('info-fieldtype', InfoFieldtype);
app.component('integer-fieldtype', IntegerFieldtype);
app.component('link-fieldtype', LinkFieldtype);
app.component('link-fieldtype-index', LinkIndexFieldtype);
diff --git a/resources/js/components/fieldtypes/InfoFieldtype.vue b/resources/js/components/fieldtypes/InfoFieldtype.vue
new file mode 100644
index 00000000000..466ae43584f
--- /dev/null
+++ b/resources/js/components/fieldtypes/InfoFieldtype.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js b/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js
new file mode 100644
index 00000000000..6a83b6edd24
--- /dev/null
+++ b/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js
@@ -0,0 +1,49 @@
+import { mount } from '@vue/test-utils';
+import { describe, expect, test } from 'vitest';
+import InfoFieldtype from '@/components/fieldtypes/InfoFieldtype.vue';
+import { Icon } from '@/components/ui';
+
+window.__ = (key) => key;
+
+function mountField(config = {}) {
+ return mount(InfoFieldtype, {
+ props: {
+ value: null,
+ handle: 'info',
+ config,
+ },
+ });
+}
+
+describe.each([
+ ['notice', 'default'],
+ ['tip', 'tip'],
+ ['warning', 'warning'],
+ ['important', 'error'],
+ ['success', 'success'],
+])('%s state', (state, variant) => {
+ test(`uses the ${variant} alert variant`, () => {
+ const wrapper = mountField({ state, content: 'Something happened.' });
+
+ expect(wrapper.get('[data-ui-alert]').attributes('data-variant')).toBe(variant);
+ });
+});
+
+test('renders sanitized markdown with links and lists', () => {
+ const wrapper = mountField({
+ content: '- First item\n- [Statamic](https://statamic.com)\n\n',
+ });
+
+ expect(wrapper.findAll('li')).toHaveLength(2);
+ expect(wrapper.get('a').attributes()).toMatchObject({
+ href: 'https://statamic.com',
+ target: '_blank',
+ });
+ expect(wrapper.find('script').exists()).toBe(false);
+});
+
+test('uses a configured icon', () => {
+ const wrapper = mountField({ content: 'Hello', icon: 'lightbulb-idea' });
+
+ expect(wrapper.findComponent(Icon).props('name')).toBe('lightbulb-idea');
+});
diff --git a/src/Fieldtypes/Info.php b/src/Fieldtypes/Info.php
new file mode 100644
index 00000000000..c982ddc2b8b
--- /dev/null
+++ b/src/Fieldtypes/Info.php
@@ -0,0 +1,60 @@
+ __('Content'),
+ 'fields' => [
+ 'content' => [
+ 'display' => __('Content'),
+ 'instructions' => __('statamic::fieldtypes.info.config.content'),
+ 'type' => 'textarea',
+ ],
+ ],
+ ],
+ [
+ 'display' => __('Appearance'),
+ 'fields' => [
+ 'state' => [
+ 'display' => __('State'),
+ 'instructions' => __('statamic::fieldtypes.info.config.state'),
+ 'type' => 'select',
+ 'default' => 'notice',
+ 'options' => [
+ 'notice' => __('Notice'),
+ 'tip' => __('Tip'),
+ 'warning' => __('Warning'),
+ 'important' => __('Important Warning'),
+ 'success' => __('Success'),
+ ],
+ 'width' => 50,
+ ],
+ 'icon' => [
+ 'display' => __('Icon'),
+ 'instructions' => __('statamic::fieldtypes.info.config.icon'),
+ 'type' => 'icon',
+ 'set' => 'default',
+ 'mode' => 'compact',
+ 'width' => 50,
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php
index b237ba4ae0d..8510e539540 100644
--- a/src/Providers/ExtensionServiceProvider.php
+++ b/src/Providers/ExtensionServiceProvider.php
@@ -91,6 +91,7 @@ class ExtensionServiceProvider extends ServiceProvider
Fieldtypes\Hidden::class,
Fieldtypes\Html::class,
Fieldtypes\Icon::class,
+ Fieldtypes\Info::class,
Fieldtypes\Integer::class,
Fieldtypes\Link::class,
Fieldtypes\Lists::class,
diff --git a/tests/Fieldtypes/InfoTest.php b/tests/Fieldtypes/InfoTest.php
new file mode 100644
index 00000000000..dc7f2f899ec
--- /dev/null
+++ b/tests/Fieldtypes/InfoTest.php
@@ -0,0 +1,41 @@
+assertSame(['special'], $fieldtype->categories());
+ $this->assertFalse($fieldtype->localizable());
+ $this->assertFalse($fieldtype->validatable());
+ $this->assertFalse($fieldtype->defaultable());
+ }
+
+ #[Test]
+ public function it_has_configurable_content_state_and_icon()
+ {
+ $fields = (new Info)->configFields();
+
+ $this->assertSame('textarea', $fields->get('content')->type());
+ $this->assertSame('select', $fields->get('state')->type());
+ $this->assertSame('notice', $fields->get('state')->get('default'));
+ $this->assertSame([
+ 'notice' => 'Notice',
+ 'tip' => 'Tip',
+ 'warning' => 'Warning',
+ 'important' => 'Important Warning',
+ 'success' => 'Success',
+ ], $fields->get('state')->get('options'));
+ $this->assertSame('icon', $fields->get('icon')->type());
+ $this->assertSame('default', $fields->get('icon')->get('set'));
+ $this->assertSame('compact', $fields->get('icon')->get('mode'));
+ }
+}