diff --git a/.changeset/align-banner-toast-icons.md b/.changeset/align-banner-toast-icons.md
new file mode 100644
index 0000000000..1f7bcf0f4b
--- /dev/null
+++ b/.changeset/align-banner-toast-icons.md
@@ -0,0 +1,5 @@
+---
+"@cloudflare/kumo": patch
+---
+
+Align Banner and Toast status icons to the first line of wrapping message text and prevent the icons from shrinking.
diff --git a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx
index ab14ae6ba8..d93b0a8362 100644
--- a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx
+++ b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx
@@ -101,6 +101,38 @@ export function BannerWithIconDemo() {
);
}
+export function BannerIconAlignmentPreview() {
+ const message =
+ "A DNS record for puppies.cloudflare.dev already exists in this zone and must be reviewed before continuing.";
+
+ return (
+
+ );
+}
+
/** Banner with custom React content in description. */
export function BannerCustomContentDemo() {
return (
diff --git a/packages/kumo-docs-astro/src/pages/components/banner.mdx b/packages/kumo-docs-astro/src/pages/components/banner.mdx
index 2da2ba5b35..2b475acc2b 100644
--- a/packages/kumo-docs-astro/src/pages/components/banner.mdx
+++ b/packages/kumo-docs-astro/src/pages/components/banner.mdx
@@ -15,7 +15,9 @@ import {
BannerAlertDemo,
BannerErrorDemo,
BannerSecondaryDemo,
+ BannerTitleOnlyDemo,
BannerWithIconDemo,
+ BannerIconAlignmentPreview,
BannerWithActionDemo,
BannerWithActionsDemo,
BannerCompactDemo,
@@ -111,6 +113,29 @@ export default function Example() {
+#### Title only
+
+
+
+
+
+#### Icon alignment with wrapping text
+
+Icons align to the first line of banner text and keep their size when the message wraps.
+
+}
+ variant="alert"
+ description="A DNS record already exists in this zone and must be reviewed before continuing."
+/>`}
+ vrSection="banner-icon-alignment"
+ vrTitle="Banner Icon Alignment"
+>
+
+
+
### With action
diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx
index 0faa017a28..7ffece5e6b 100644
--- a/packages/kumo/src/components/banner/banner.test.tsx
+++ b/packages/kumo/src/components/banner/banner.test.tsx
@@ -139,11 +139,28 @@ describe("Banner", () => {
expect(className).toContain("px-3");
expect(className).toContain("py-2");
expect(className).toContain("text-sm");
- // Compact banners align everything on one centered row.
- expect(className).toContain("items-center");
+ // Compact banners align icons to the first text line when content wraps.
+ expect(className).toContain("items-start");
// Base-size spacing/alignment must not leak in.
expect(className).not.toContain("px-4");
- expect(className).not.toContain("items-start");
+ });
+
+ it("aligns icons to the first text line without shrinking", () => {
+ render(
+ }
+ description="A DNS record already exists in this zone and may wrap onto multiple lines."
+ />,
+ );
+
+ const icon = screen.getByTestId("icon");
+ const iconClassName = icon.getAttribute("class") ?? "";
+ expect(iconClassName).toContain("size-[1em]");
+ expect(iconClassName).toContain("flex-none");
+ expect(iconClassName).toContain("custom-icon");
+ expect(icon.parentElement?.className).toContain("h-[1lh]");
+ expect(icon.parentElement?.className).toContain("flex-none");
});
it("defaults Banner.Action children to xs in an sm banner", () => {
diff --git a/packages/kumo/src/components/banner/banner.tsx b/packages/kumo/src/components/banner/banner.tsx
index 4e956c7c4f..3866f36d55 100644
--- a/packages/kumo/src/components/banner/banner.tsx
+++ b/packages/kumo/src/components/banner/banner.tsx
@@ -1,6 +1,8 @@
import {
type HTMLAttributes,
+ type ReactElement,
type ReactNode,
+ cloneElement,
forwardRef,
isValidElement,
} from "react";
@@ -46,7 +48,7 @@ export const KUMO_BANNER_VARIANTS = {
description: "Default banner size",
},
sm: {
- classes: "items-center gap-2 rounded-md px-3 py-2 text-sm",
+ classes: "items-start gap-2 rounded-md px-3 py-2 text-sm",
description: "Compact banner for dialogs and tight spaces",
},
},
@@ -63,28 +65,51 @@ export type KumoBannerSize = keyof typeof KUMO_BANNER_VARIANTS.size;
/**
* Per-size render-site classes not carried by `bannerVariants` (which only emits
- * the container classes). `row` is the title↔action flex gap, `icon` the icon
- * wrapper height, `description` the description text size, and `action` the size
+ * the container classes). `row` is the title↔action flex gap,
+ * `description` the description text size, and `action` the size
* that child `Banner.Action`s inherit via {@link BannerActionContext}.
*/
const BANNER_SIZE_PARTS: Record<
KumoBannerSize,
- { row: string; icon: string; description: string; action: BannerActionSize }
+ { row: string; description: string; action: BannerActionSize }
> = {
base: {
row: "gap-3",
- icon: "h-[1.375em]",
description: "text-sm",
action: "sm",
},
sm: {
row: "gap-2",
- icon: "h-[1.25em]",
description: "text-sm",
action: "xs",
},
};
+const renderBannerIcon = (icon: ReactNode, className?: string) => {
+ const iconElement = isValidElement(icon)
+ ? (icon as ReactElement<{ className?: string }>)
+ : null;
+ const alignedIcon = iconElement
+ ? cloneElement(iconElement, {
+ className: cn(
+ iconElement.props.className,
+ "size-[1em] flex-none leading-snug",
+ ),
+ })
+ : icon;
+
+ return (
+
+ {alignedIcon}
+
+ );
+};
+
// The `Banner.Action` CTA compound lives in ./banner-action
// and is attached to `Banner` via Object.assign at the bottom of this file.
export type {
@@ -253,17 +278,7 @@ const BannerRoot = forwardRef(function BannerRoot(
className={cn(bannerVariants({ variant, size }), className)}
{...props}
>
- {icon && (
-
- {icon}
-
- )}
+ {icon && renderBannerIcon(icon, variantConfig.iconClasses)}
(function BannerRoot(
className={cn(bannerVariants({ variant, size }), className)}
{...props}
>
- {icon && (
-
- {icon}
-
- )}
+ {icon && renderBannerIcon(icon, variantConfig.iconClasses)}
{content}
diff --git a/packages/kumo/src/components/toast/toast.tsx b/packages/kumo/src/components/toast/toast.tsx
index 29e1b2f5ad..d5041161fe 100644
--- a/packages/kumo/src/components/toast/toast.tsx
+++ b/packages/kumo/src/components/toast/toast.tsx
@@ -443,6 +443,8 @@ function ToastIcon({ variant }: { variant?: KumoToastVariant }) {
if (!("icon" in variantConfig)) return null;
const Icon = variantConfig.icon;
return (
-
+
+
+
);
}