diff --git a/app/javascript/mastodon/components/callout/redesign.module.scss b/app/javascript/mastodon/components/callout/redesign.module.scss
new file mode 100644
index 00000000000..08917807b14
--- /dev/null
+++ b/app/javascript/mastodon/components/callout/redesign.module.scss
@@ -0,0 +1,28 @@
+@use '@/styles/mastodon/mixins';
+
+.rootLarge,
+.rootSmall {
+ align-items: center;
+ background-color: var(--color-bg-highlight);
+ border-radius: var(--radius-sm);
+ box-sizing: border-box;
+ display: flex;
+ gap: var(--space-xs);
+}
+
+.rootLarge {
+ padding: var(--space-sm);
+}
+
+.rootSmall {
+ padding: var(--space-xs);
+}
+
+.textWrapper {
+ flex-grow: 1;
+}
+
+.icon {
+ width: var(--space-lg);
+ height: var(--space-lg);
+}
diff --git a/app/javascript/mastodon/components/callout/redesign.stories.tsx b/app/javascript/mastodon/components/callout/redesign.stories.tsx
new file mode 100644
index 00000000000..2a91f5be9a6
--- /dev/null
+++ b/app/javascript/mastodon/components/callout/redesign.stories.tsx
@@ -0,0 +1,58 @@
+import { InfoIcon } from '@phosphor-icons/react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { fn } from 'storybook/test';
+
+import type { CalloutProps } from './redesign';
+import { Callout } from './redesign';
+
+const meta = {
+ title: 'Redesign/Callout',
+ render(props) {
+ const actionProps = props.actionText
+ ? fn().mockName('actionClick')
+ : undefined;
+ const secondaryActionProps = props.secondaryActionText
+ ? fn().mockName('secondaryActionClick')
+ : undefined;
+ return (
+
+ );
+ },
+ args: {
+ children: 'Main text goes here',
+ actionText: 'Action 1',
+ secondaryActionText: 'Action 2',
+ hasIcon: false,
+ },
+ argTypes: {
+ children: { control: 'text' },
+ size: { control: 'inline-radio', options: ['sm', 'lg'] },
+ actionText: { control: 'text' },
+ secondaryActionText: { control: 'text', if: { arg: 'size', neq: 'sm' } },
+ hasIcon: { control: 'boolean', if: { arg: 'size', neq: 'sm' } },
+ },
+} satisfies Meta<
+ CalloutProps & {
+ as?: never;
+ hasIcon?: boolean;
+ children: string;
+ }
+>;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Large: Story = {
+ args: {
+ size: 'lg',
+ hasIcon: true,
+ },
+};
+
+export const Small: Story = { args: { size: 'sm' } };
diff --git a/app/javascript/mastodon/components/callout/redesign.tsx b/app/javascript/mastodon/components/callout/redesign.tsx
new file mode 100644
index 00000000000..982e50a1420
--- /dev/null
+++ b/app/javascript/mastodon/components/callout/redesign.tsx
@@ -0,0 +1,95 @@
+import type React from 'react';
+
+import classNames from 'classnames';
+
+import { Button } from '../button/redesign';
+import { Icon } from '../icon';
+import type { IconProp } from '../icon';
+
+import classes from './redesign.module.scss';
+
+type RenameKeysStartingWith<
+ TObject extends Record,
+ TPrefix extends string,
+ TReplacement extends string,
+> = {
+ [TKey in keyof TObject as TKey extends `${TPrefix}${infer TSuffix}`
+ ? `${TReplacement}${TSuffix}`
+ : TKey]: TObject[TKey];
+};
+
+type ActionProps =
+ | { actionClick: () => void; actionText: React.ReactNode }
+ | { actionClick?: never; actionText?: never };
+
+type MiniCalloutProps = {
+ size: 'sm';
+} & ActionProps;
+
+type LargeCalloutProps = {
+ size?: 'lg';
+ icon?: IconProp;
+} & (
+ | (ActionProps & {
+ secondaryActionClick?: never;
+ secondaryActionText?: never;
+ })
+ | (Required &
+ RenameKeysStartingWith)
+);
+
+export type CalloutProps = (
+ | MiniCalloutProps
+ | LargeCalloutProps
+) & {
+ as?: As;
+ children: React.ReactNode;
+ className?: string;
+} & React.ComponentPropsWithoutRef;
+
+export const Callout = ({
+ as: asComp,
+ size,
+ children,
+ className,
+ actionText,
+ actionClick,
+ ...props
+}: CalloutProps) => {
+ const Comp = asComp ?? 'div';
+
+ const wrappedChildren = {children}
;
+
+ const actionButton = actionText ? (
+
+ ) : null;
+
+ if (size === 'sm') {
+ return (
+
+ {wrappedChildren}
+ {actionButton}
+
+ );
+ }
+
+ const { icon, secondaryActionClick, secondaryActionText, ...restProps } =
+ props;
+
+ const secondaryButton = secondaryActionText ? (
+
+ ) : null;
+
+ return (
+
+ {icon && }
+ {wrappedChildren}
+ {actionButton}
+ {secondaryButton}
+
+ );
+};
diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx
index f93e3757a38..688a5a5781f 100644
--- a/app/javascript/mastodon/components/icon.tsx
+++ b/app/javascript/mastodon/components/icon.tsx
@@ -11,7 +11,7 @@ export type IconProp = React.FC;
interface Props extends React.SVGProps {
children?: never;
- id: string;
+ id?: string;
icon: IconProp;
noFill?: boolean;
}
@@ -46,7 +46,7 @@ export const Icon: React.FC = ({