From c343cb2327f4e639616372310a8ac9ce489d7edb Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Tue, 11 Aug 2026 09:47:17 -0400 Subject: [PATCH 1/3] fix: align banner and toast icons --- .changeset/align-banner-toast-icons.md | 5 ++ .../src/components/demos/BannerDemo.tsx | 26 +++++++++ .../src/pages/components/banner.mdx | 18 ++++++ .../src/components/banner/banner.test.tsx | 23 +++++++- .../kumo/src/components/banner/banner.tsx | 55 +++++++++++-------- packages/kumo/src/components/toast/toast.tsx | 2 +- 6 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 .changeset/align-banner-toast-icons.md 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..e207f14a53 100644 --- a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx @@ -101,6 +101,32 @@ 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 ( +
+
+

Before

+
+ +

{message}

+
+
+
+

After

+
+ + + +

{message}

+
+
+
+ ); +} + /** 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..eb8623c257 100644 --- a/packages/kumo-docs-astro/src/pages/components/banner.mdx +++ b/packages/kumo-docs-astro/src/pages/components/banner.mdx @@ -16,6 +16,7 @@ import { BannerErrorDemo, BannerSecondaryDemo, BannerWithIconDemo, + BannerIconAlignmentPreview, BannerWithActionDemo, BannerWithActionsDemo, BannerCompactDemo, @@ -111,6 +112,23 @@ export default function Example() { +#### 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..52e80b1943 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("h-[1lh]"); + 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..346a47c054 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, + "h-[1lh] 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..e0272fceb4 100644 --- a/packages/kumo/src/components/toast/toast.tsx +++ b/packages/kumo/src/components/toast/toast.tsx @@ -443,6 +443,6 @@ function ToastIcon({ variant }: { variant?: KumoToastVariant }) { if (!("icon" in variantConfig)) return null; const Icon = variantConfig.icon; return ( - + ); } From 103441d904a7c026dc7ec1dd07906237ff4cd9c0 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Tue, 11 Aug 2026 09:57:01 -0400 Subject: [PATCH 2/3] fix: center status icons within line-height slot --- .../src/components/demos/BannerDemo.tsx | 12 +++++++++--- packages/kumo/src/components/banner/banner.test.tsx | 2 +- packages/kumo/src/components/banner/banner.tsx | 2 +- packages/kumo/src/components/toast/toast.tsx | 4 +++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx index e207f14a53..d93b0a8362 100644 --- a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx @@ -110,15 +110,21 @@ export function BannerIconAlignmentPreview() {

Before

- +

{message}

After

- - + +

{message}

diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx index 52e80b1943..7ffece5e6b 100644 --- a/packages/kumo/src/components/banner/banner.test.tsx +++ b/packages/kumo/src/components/banner/banner.test.tsx @@ -156,7 +156,7 @@ describe("Banner", () => { const icon = screen.getByTestId("icon"); const iconClassName = icon.getAttribute("class") ?? ""; - expect(iconClassName).toContain("h-[1lh]"); + expect(iconClassName).toContain("size-[1em]"); expect(iconClassName).toContain("flex-none"); expect(iconClassName).toContain("custom-icon"); expect(icon.parentElement?.className).toContain("h-[1lh]"); diff --git a/packages/kumo/src/components/banner/banner.tsx b/packages/kumo/src/components/banner/banner.tsx index 346a47c054..3866f36d55 100644 --- a/packages/kumo/src/components/banner/banner.tsx +++ b/packages/kumo/src/components/banner/banner.tsx @@ -93,7 +93,7 @@ const renderBannerIcon = (icon: ReactNode, className?: string) => { ? cloneElement(iconElement, { className: cn( iconElement.props.className, - "h-[1lh] flex-none leading-snug", + "size-[1em] flex-none leading-snug", ), }) : icon; diff --git a/packages/kumo/src/components/toast/toast.tsx b/packages/kumo/src/components/toast/toast.tsx index e0272fceb4..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 ( - + + + ); } From 74ef58b429b3130363c6a1d72857f389526fd59e Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Tue, 11 Aug 2026 09:59:34 -0400 Subject: [PATCH 3/3] docs: show title-only banner example --- packages/kumo-docs-astro/src/pages/components/banner.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/kumo-docs-astro/src/pages/components/banner.mdx b/packages/kumo-docs-astro/src/pages/components/banner.mdx index eb8623c257..2b475acc2b 100644 --- a/packages/kumo-docs-astro/src/pages/components/banner.mdx +++ b/packages/kumo-docs-astro/src/pages/components/banner.mdx @@ -15,6 +15,7 @@ import { BannerAlertDemo, BannerErrorDemo, BannerSecondaryDemo, + BannerTitleOnlyDemo, BannerWithIconDemo, BannerIconAlignmentPreview, BannerWithActionDemo, @@ -112,6 +113,12 @@ 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.