diff --git a/src/control-presentation/control-action-button.test.tsx b/src/control-presentation/control-action-button.test.tsx
new file mode 100644
index 00000000..0c76aab6
--- /dev/null
+++ b/src/control-presentation/control-action-button.test.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react'
+
+import { render, screen } from '@testing-library/react'
+
+import { ControlActionButton } from './control-action-button'
+
+import styles from './control-presentation.module.css'
+
+describe('ControlActionButton', () => {
+ it('renders the text-label Button branch with compact field styling', () => {
+ render(Clear)
+
+ const button = screen.getByRole('button', { name: 'Clear' })
+ expect(button).toHaveClass(styles.controlActionButton!)
+ })
+
+ it('renders the icon-only IconButton branch with compact field styling', () => {
+ render()
+
+ const button = screen.getByRole('button', { name: 'Clear' })
+ expect(button).toHaveClass(styles.controlActionButton!)
+ })
+})
diff --git a/src/control-presentation/control-action-button.tsx b/src/control-presentation/control-action-button.tsx
new file mode 100644
index 00000000..0ba71b94
--- /dev/null
+++ b/src/control-presentation/control-action-button.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react'
+
+import classNames from 'classnames'
+
+import { Button, IconButton } from '../button'
+
+import styles from './control-presentation.module.css'
+
+import type { ButtonProps, IconButtonProps } from '../button'
+
+export type ControlActionButtonProps =
+ | ({
+ children: NonNullable
+ icon?: never
+ } & Omit)
+ | ({
+ icon: IconButtonProps['icon']
+ children?: never
+ } & Omit)
+
+/**
+ * A compact action button intended for `ControlPresentation`'s `endSlot`. Wraps
+ * Reactist's `Button` / `IconButton` with a 24×24, 3px-radius variant sized to fit
+ * the field chrome alongside a 16px icon glyph.
+ */
+export const ControlActionButton = React.forwardRef(
+ function ControlActionButton({ exceptionallySetClassName, ...props }, ref) {
+ const sharedProps = {
+ ref,
+ variant: 'quaternary' as const,
+ exceptionallySetClassName: classNames([
+ styles.controlActionButton,
+ exceptionallySetClassName,
+ ]),
+ }
+
+ return 'children' in props ? (
+
+ ) : (
+
+ )
+ },
+)
diff --git a/src/control-presentation/control-presentation.module.css b/src/control-presentation/control-presentation.module.css
new file mode 100644
index 00000000..d4a61ab2
--- /dev/null
+++ b/src/control-presentation/control-presentation.module.css
@@ -0,0 +1,66 @@
+:root {
+ --reactist-field-height: 32px;
+}
+
+.container {
+ /* sizing */
+ height: var(--reactist-field-height);
+
+ /* slot-to-control gap (only takes effect between rendered children) */
+ gap: 6px;
+
+ /* default outer padding; shrunk on the side(s) where a slot is present */
+ padding-inline: 10px;
+}
+
+/* Conditional outer padding. When a slot is present on a side, shrink
+ * the outer padding on that side; the wrapper's flex `gap` then provides
+ * the visual spacing between the slot and the control. */
+.container:has(.startSlot) {
+ padding-left: 6px;
+}
+
+.container:has(.endSlot) {
+ padding-right: 4px;
+}
+
+.control {
+ display: contents;
+}
+
+/* The wrapped control inherits chrome styling so that native elements
+ * (input/select/textarea) render flush with the surrounding wrapper. */
+.control > * {
+ /* layout */
+ flex: 1;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+
+ /* border */
+ border: none;
+ outline: none; /* focus state is handled by the wrapper border */
+
+ /* color */
+ background: transparent;
+ color: inherit;
+}
+
+.slot {
+ /* color */
+ color: var(--reactist-field-slot-content);
+}
+
+/*
+ * Compact 24×24 action button variant for use inside `endSlot`. The 3px
+ * border-radius and reduced min-width make it fit the field chrome alongside a
+ * 16px icon glyph. The tripled class boosts specificity above Button's
+ * size-class rules (e.g. `.baseButton.size-normal`) so the height override
+ * wins regardless of stylesheet load order.
+ */
+.controlActionButton.controlActionButton.controlActionButton {
+ --reactist-btn-height: 24px;
+ border-radius: 3px;
+ min-width: var(--reactist-btn-height);
+}
diff --git a/src/control-presentation/control-presentation.stories.tsx b/src/control-presentation/control-presentation.stories.tsx
new file mode 100644
index 00000000..86fe0d98
--- /dev/null
+++ b/src/control-presentation/control-presentation.stories.tsx
@@ -0,0 +1,280 @@
+import * as React from 'react'
+
+import { Box } from '../box'
+import { selectWithNone } from '../utils/storybook-helper'
+
+import { ControlActionButton } from './control-action-button'
+import { ControlPresentation } from './control-presentation'
+
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import type { ComponentProps } from 'react'
+
+const meta = {
+ title: 'Design system/ControlPresentation',
+ component: ControlPresentation,
+ parameters: {
+ badges: ['accessible'],
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+type PlaygroundArgs = {
+ startSlot: boolean
+ endSlot: boolean
+ readOnly: boolean
+ disabled: boolean
+ invalid: boolean
+ placeholder: string
+ defaultValue: string
+ maxWidth: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'full'
+}
+
+function PlaygroundTemplate({
+ startSlot,
+ endSlot,
+ readOnly,
+ disabled,
+ invalid,
+ placeholder,
+ defaultValue,
+ maxWidth,
+}: PlaygroundArgs) {
+ return (
+
+ )
+}
+
+export const Playground: StoryObj = {
+ name: 'Playground',
+ render: PlaygroundTemplate,
+ args: {
+ startSlot: true,
+ endSlot: true,
+ readOnly: false,
+ disabled: false,
+ invalid: false,
+ placeholder: 'Select a value',
+ defaultValue: '',
+ maxWidth: 'small',
+ },
+ argTypes: {
+ maxWidth: selectWithNone(['xsmall', 'small', 'medium', 'large', 'xlarge', 'full'], 'small'),
+ startSlot: { control: { type: 'boolean' } },
+ endSlot: { control: { type: 'boolean' } },
+ readOnly: { control: { type: 'boolean' } },
+ disabled: { control: { type: 'boolean' } },
+ invalid: { control: { type: 'boolean' } },
+ placeholder: { control: { type: 'text' } },
+ defaultValue: { control: { type: 'text' } },
+ },
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const Placeholder: Story = {
+ name: 'Placeholder',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const WithValue: Story = {
+ name: 'With value',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const ReadOnly: Story = {
+ name: 'Read-only',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const Disabled: Story = {
+ name: 'Disabled',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const Error: Story = {
+ name: 'Error (aria-invalid)',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const EndSlotAsUnit: Story = {
+ name: 'endSlot as trailing unit',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const EndSlotAsClearButton: Story = {
+ name: 'endSlot with clear button',
+ render: () => (
+
+ ),
+ parameters: {
+ chromatic: { disableSnapshot: false },
+ },
+}
+
+export const WrappingSelect: Story = {
+ name: 'Wrapping a