diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts
new file mode 100644
index 000000000000..cc57ff246196
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts
@@ -0,0 +1,80 @@
+import { css, customElement, html, property, LitElement } from '@umbraco-cms/backoffice/external/lit';
+
+/**
+ * An overlay that frames its parent with a rounded border and shows a label tab
+ * just above the parent's top-right corner. Visibility is controlled by the consumer via
+ * `--umb-entity-frame-opacity` (defaults to `1`); the typical pattern is for the parent
+ * container to set it to `0` by default and toggle to `1` on `:hover` and/or `:focus-within`.
+ * The parent must establish a positioning context (e.g. `position: relative`), and must not
+ * clip overflow above its top edge (the tab renders outside the parent's content box).
+ * @element umb-entity-frame
+ * @slot - Optional rich content for the tab. Falls back to the `label` property.
+ * @cssprop --umb-entity-frame-border-width - Thickness of the border. Defaults to `2px`.
+ * @cssprop --umb-entity-frame-color - Accent colour for the border and tab background. Defaults to `--uui-color-focus`.
+ * @cssprop --umb-entity-frame-contrast-color - Text colour for the tab. Defaults to `--uui-color-selected-contrast`. Override when using a custom `--umb-entity-frame-color` that does not pair with the default contrast token.
+ * @cssprop --umb-entity-frame-opacity - Opacity of the border and tab. Defaults to `1`. Set to `0` on the parent and toggle to `1` on `:hover` / `:focus-within` to gate visibility.
+ * @augments {LitElement}
+ */
+@customElement('umb-entity-frame')
+export class UmbEntityFrameElement extends LitElement {
+ /**
+ * Text displayed in the tab when no slot content is projected.
+ * @type {string}
+ * @attr
+ * @default ''
+ */
+ @property({ type: String })
+ label: string = '';
+
+ override render() {
+ return html`
+
+ ${this.label}
+ `;
+ }
+
+ static override readonly styles = [
+ css`
+ :host {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 1;
+ }
+
+ .border,
+ .tab {
+ opacity: var(--umb-entity-frame-opacity, 1);
+ transition: opacity 120ms ease-out;
+ }
+
+ .border {
+ position: absolute;
+ inset: 0;
+ border: var(--umb-entity-frame-border-width, 2px) solid var(--umb-entity-frame-color, var(--uui-color-focus));
+ border-radius: var(--uui-border-radius);
+ border-top-right-radius: 0;
+ box-sizing: border-box;
+ pointer-events: none;
+ }
+
+ .tab {
+ position: absolute;
+ bottom: 100%;
+ right: 0;
+ background: var(--umb-entity-frame-color, var(--uui-color-focus));
+ color: var(--umb-entity-frame-contrast-color, var(--uui-color-selected-contrast));
+ padding: var(--uui-size-2) var(--uui-size-2) var(--uui-size-1);
+ border-radius: var(--uui-border-radius) var(--uui-border-radius) 0 0;
+ font-size: var(--uui-type-small-size);
+ line-height: 1;
+ }
+ `,
+ ];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-entity-frame': UmbEntityFrameElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts
new file mode 100644
index 000000000000..acad6e1a636c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts
@@ -0,0 +1,194 @@
+import { html } from '@umbraco-cms/backoffice/external/lit';
+import type { UmbEntityFrameElement } from './entity-frame.element.js';
+import type { Meta, StoryObj } from '@storybook/web-components-vite';
+
+import './entity-frame.element.js';
+
+const meta: Meta = {
+ component: 'umb-entity-frame',
+ title: 'Generic Components/Entity Frame',
+ args: {
+ label: 'Document: Hero Banner',
+ },
+ decorators: [(story) => html`${story()}
`],
+ render: (args) => html`
+
+
Entity label tab with full opacity.
+
+
+ `,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Docs: Story = {};
+
+export const OnHover: Story = {
+ render: (args) => html`
+
+
+
Hover this container to reveal.
+
+
+ `,
+};
+
+export const OnHoverOrFocus: Story = {
+ render: (args) => html`
+
+
+
Hover, or Tab into the button below.
+
Focusable child
+
+
+ `,
+};
+
+export const WithSlot: Story = {
+ render: () => html`
+
+
Slotted content overrides the label.
+
+
+ Document: Hero Banner
+
+
+ `,
+};
+
+export const WrappingButton: Story = {
+ render: () => html`
+
+
+
+ Edit Hero Banner
+
+
+
+ `,
+};
+
+export const Nested: Story = {
+ render: () => html`
+
+
+
Outer container (both visible when hovering inner)
+
+
+
+
Inner container (only inner visible without hovering outer)
+
+
+
+ `,
+};
+
+export const WithCustomColor: Story = {
+ render: () => html`
+
+
Themed via --umb-entity-frame-color.
+
+
+ `,
+};
+
+export const ReferenceList: Story = {
+ render: () => {
+ const items = [
+ { label: 'Home', color: 'maroon' },
+ { label: 'About', color: 'green' },
+ { label: 'Contact', color: 'blue' },
+ { label: 'Blog', color: 'purple' },
+ ];
+
+ return html`
+
+
+ ${items.map(
+ (item) => html`
+
+
+
+ `,
+ )}
+
+ `;
+ },
+};
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts
new file mode 100644
index 000000000000..8c7b4a89c49c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts
@@ -0,0 +1,53 @@
+import { UmbEntityFrameElement } from './entity-frame.element.js';
+import { expect, fixture, html } from '@open-wc/testing';
+import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
+
+describe('UmbEntityFrameElement', () => {
+ let element: UmbEntityFrameElement;
+
+ beforeEach(async () => {
+ element = await fixture(html``);
+ });
+
+ it('is defined with its own instance', () => {
+ expect(element).to.be.instanceOf(UmbEntityFrameElement);
+ });
+
+ it('renders a border element', () => {
+ const border = element.shadowRoot!.querySelector('.border');
+ expect(border).to.not.equal(null);
+ });
+
+ it('renders a tab with a default slot', () => {
+ const slot = element.shadowRoot!.querySelector('.tab slot');
+ expect(slot).to.not.equal(null);
+ });
+
+ it('has a label property that defaults to an empty string', () => {
+ expect(element.label).to.equal('');
+ });
+
+ it('renders the label inside the tab when no slot content is projected', async () => {
+ element.label = 'Document: Hero';
+ await element.updateComplete;
+ const tab = element.shadowRoot!.querySelector('.tab')!;
+ expect(tab.textContent?.trim()).to.equal('Document: Hero');
+ });
+
+ it('renders slot content in preference to the label property', async () => {
+ const slotted = await fixture(
+ html`Slotted`,
+ );
+ const slot = slotted.shadowRoot!.querySelector('.tab slot') as HTMLSlotElement;
+ const assigned = slot.assignedNodes({ flatten: true });
+ expect(assigned.length).to.be.greaterThan(0);
+ const projected = assigned.find((node): node is HTMLElement => node instanceof HTMLElement);
+ expect(projected?.textContent?.trim()).to.equal('Slotted');
+ });
+
+ if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
+ it('passes the a11y audit', async () => {
+ await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
+ });
+ }
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts
new file mode 100644
index 000000000000..8efc97ba542f
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts
@@ -0,0 +1,2 @@
+import './entity-frame.element.js';
+export * from './entity-frame.element.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
index 24b71521f6ec..94d225a3bfd3 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
@@ -8,6 +8,7 @@ export * from './figure-card/figure-card.element.js';
export * from './code-block/index.js';
export * from './dropdown/index.js';
export * from './entity-actions-bundle/index.js';
+export * from './entity-frame/index.js';
export * from './footer-layout/index.js';
export * from './header-app/index.js';
export * from './history/index.js';