From a91479646ccef4815a1a18a41946580b6e928f5c Mon Sep 17 00:00:00 2001 From: anmol Date: Mon, 27 Jul 2026 21:27:25 +0530 Subject: [PATCH] fix(banner): quieten and brighten the Banner.Action CTA `Banner.Action` defaulted to `variant="primary"`, so every banner rendered a saturated filled accent button that overpowered its own message and competed with page-level CTAs. - Default to `variant="secondary"`: an accent-tinted fill with an accent hairline. `variant="primary"` stays available for the rare banner whose CTA is the most important action on the page. - Brighten the `secondary` and `ghost` accents. Labels and icons now use the banner's accent text token directly instead of inheriting the container colour, which picked up the neutral banner's dimmed `text-kumo-default/70` and made the CTA look muddy. - Gate hover colours behind `not-disabled:`, so hover is inert on disabled and loading actions. This also drops the `!important` hovers, since the modifier set now matches Button's own outline defaults and tailwind-merge dedupes them. --- .changeset/quiet-banner-action-cta.md | 9 +++ .../src/components/demos/BannerDemo.tsx | 30 +++++++- .../src/pages/components/banner.mdx | 13 ++++ .../src/components/banner/banner-action.tsx | 56 ++++++++++---- .../src/components/banner/banner.test.tsx | 73 ++++++++++++++++--- .../kumo/src/components/banner/banner.tsx | 3 +- 6 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 .changeset/quiet-banner-action-cta.md diff --git a/.changeset/quiet-banner-action-cta.md b/.changeset/quiet-banner-action-cta.md new file mode 100644 index 0000000000..4588ddda3e --- /dev/null +++ b/.changeset/quiet-banner-action-cta.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/kumo": patch +--- + +Softens the `Banner.Action` CTA so it no longer overpowers the banner's message. + +- `Banner.Action` now defaults to `variant="secondary"` instead of `variant="primary"`. The default treatment is an accent-tinted fill with an accent hairline, which reads as a button without competing with page-level CTAs. Opt back in with `variant="primary"` when the banner's action is the single most important one on the page. +- Brightened the `secondary` and `ghost` accent colors. Labels and icons now use the banner's accent text token directly rather than inheriting the container color, which previously picked up the neutral banner's dimmed `text-kumo-default/70` and made the CTA look muddy. +- Gated all `Banner.Action` hover colors behind `not-disabled:`, so hover styling stays inert on disabled and loading actions. diff --git a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx index ab14ae6ba8..5ad8b2253d 100644 --- a/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/BannerDemo.tsx @@ -171,7 +171,10 @@ export function BannerWithActionDemo() { ); } -/** Banner with multiple action buttons. */ +/** + * Banner with multiple action buttons. The leading CTA keeps the default quiet + * accent chip; supporting actions step down to `variant="ghost"`. + */ export function BannerWithActionsDemo() { return (
@@ -183,7 +186,30 @@ export function BannerWithActionsDemo() { action={ <> Pay now - Go to billing + Go to billing + + } + /> +
+ ); +} + +/** + * Escalate to `variant="primary"` only when the banner's CTA is the single most + * important action on the page — a filled accent button competes with page-level CTAs. + */ +export function BannerActionEmphasisDemo() { + return ( +
+ } + variant="error" + title="Your account is 90 days past due." + description="Pay now to avoid interruption." + action={ + <> + Pay now + Go to billing } /> diff --git a/packages/kumo-docs-astro/src/pages/components/banner.mdx b/packages/kumo-docs-astro/src/pages/components/banner.mdx index 2da2ba5b35..aced78e16b 100644 --- a/packages/kumo-docs-astro/src/pages/components/banner.mdx +++ b/packages/kumo-docs-astro/src/pages/components/banner.mdx @@ -18,6 +18,7 @@ import { BannerWithIconDemo, BannerWithActionDemo, BannerWithActionsDemo, + BannerActionEmphasisDemo, BannerCompactDemo, BannerCompactWithCtaDemo, BannerCompactWithoutActionDemo, @@ -113,16 +114,28 @@ export default function Example() { ### With action +`Banner.Action` picks up the banner's accent color automatically. It defaults to `variant="secondary"` — an accent-tinted fill with an accent hairline — so the CTA stays readable without shouting over the message. + ### With multiple actions +Keep the leading CTA on the default treatment and step supporting actions down to `variant="ghost"`. + +### High-emphasis action + +Escalate to `variant="primary"` only when the banner's CTA is the single most important action on the page — a filled accent button competes with page-level CTAs. + + + + + ### Compact size Use `size="sm"` in dialogs and other space-constrained contexts. Here, a banner should support the surrounding experience without competing with its primary action, so prefer a Kumo `Link` that renders inline with the description. When greater emphasis is warranted, use `Banner.Action`, which renders at the trailing end. diff --git a/packages/kumo/src/components/banner/banner-action.tsx b/packages/kumo/src/components/banner/banner-action.tsx index 4df98f62ca..b53704048b 100644 --- a/packages/kumo/src/components/banner/banner-action.tsx +++ b/packages/kumo/src/components/banner/banner-action.tsx @@ -15,8 +15,9 @@ import type { KumoBannerVariant } from "./banner"; /** * Visual variant for a `Banner.Action`, aligned with `Button`'s `variant` naming. - * - `"primary"` — filled accent gradient for the main action. - * - `"secondary"` — transparent with an accent-hued outline (same hue as the banner). + * - `"secondary"` — accent-tinted fill with an accent hairline (default). + * - `"primary"` — filled accent gradient; reserve for banners whose CTA is the + * single most important action on the page, as it competes with page-level CTAs. * - `"ghost"` — text-only accent action with a faint accent-tinted hover. */ export type BannerActionVariant = Extract< @@ -55,7 +56,22 @@ export const BannerActionContext = createContext({ size: "sm", }); -/** Per-banner-variant colors passed to the underlying `Button`. */ +/** + * Per-banner-variant colors passed to the underlying `Button`. + * + * `secondary` is the default CTA treatment: an accent-tinted fill over the banner's + * already-tinted surface plus an accent hairline, so the action reads as a button + * without shouting over the message. Hover deepens both the fill and the hairline. + * + * The label uses the explicit `text-kumo-*` token rather than `text-inherit`. For the + * accent banners this matches what they already set on the container, but the neutral + * `secondary` banner dims its body text to `text-kumo-default/70`, and inheriting that + * made the chip read muddy against the tinted fill. + * + * Hover colors are prefixed with `not-disabled:` so they match the modifier set on + * `Button`'s own `not-disabled:hover:*` outline defaults — that lets tailwind-merge + * dedupe them and keeps hover inert on a disabled or loading action. + */ const BANNER_ACTION_ACCENTS: Record< KumoBannerVariant, { accent: string; secondary: string; ghost: string } @@ -63,26 +79,30 @@ const BANNER_ACTION_ACCENTS: Record< default: { accent: "var(--color-kumo-info)", secondary: - "text-inherit ring-kumo-info/50 fill-kumo-info hover:!text-inherit hover:!ring-kumo-info/50 hover:bg-kumo-info/10", - ghost: "text-inherit fill-kumo-info hover:bg-kumo-info/10", + "bg-kumo-info/12 text-kumo-info ring-kumo-info/40 fill-kumo-info not-disabled:hover:text-kumo-info not-disabled:hover:ring-kumo-info/60 not-disabled:hover:bg-kumo-info/20", + ghost: + "text-kumo-info fill-kumo-info not-disabled:hover:bg-kumo-info/12 not-disabled:hover:text-kumo-info", }, alert: { accent: "var(--color-kumo-warning)", secondary: - "text-inherit ring-kumo-warning/50 fill-kumo-warning hover:!text-inherit hover:!ring-kumo-warning/50 hover:bg-kumo-warning/10", - ghost: "text-inherit fill-kumo-warning hover:bg-kumo-warning/10", + "bg-kumo-warning/12 text-kumo-warning ring-kumo-warning/40 fill-kumo-warning not-disabled:hover:text-kumo-warning not-disabled:hover:ring-kumo-warning/60 not-disabled:hover:bg-kumo-warning/20", + ghost: + "text-kumo-warning fill-kumo-warning not-disabled:hover:bg-kumo-warning/12 not-disabled:hover:text-kumo-warning", }, error: { accent: "var(--color-kumo-danger)", secondary: - "text-inherit ring-kumo-danger/50 fill-kumo-danger hover:!text-inherit hover:!ring-kumo-danger/50 hover:bg-kumo-danger/10", - ghost: "text-inherit fill-kumo-danger hover:bg-kumo-danger/10", + "bg-kumo-danger/12 text-kumo-danger ring-kumo-danger/40 fill-kumo-danger not-disabled:hover:text-kumo-danger not-disabled:hover:ring-kumo-danger/60 not-disabled:hover:bg-kumo-danger/20", + ghost: + "text-kumo-danger fill-kumo-danger not-disabled:hover:bg-kumo-danger/12 not-disabled:hover:text-kumo-danger", }, secondary: { accent: "var(--color-neutral-700, oklch(37.1% 0 0))", secondary: - "text-inherit ring-kumo-focus/20 fill-kumo-subtle hover:!text-inherit hover:!ring-kumo-focus/20 hover:bg-kumo-contrast/10", - ghost: "text-inherit fill-kumo-subtle hover:bg-kumo-contrast/10", + "bg-kumo-contrast/8 text-kumo-default ring-kumo-focus/30 fill-kumo-subtle not-disabled:hover:text-kumo-strong not-disabled:hover:ring-kumo-focus/45 not-disabled:hover:bg-kumo-contrast/12", + ghost: + "text-kumo-default fill-kumo-subtle not-disabled:hover:bg-kumo-contrast/10 not-disabled:hover:text-kumo-strong", }, }; @@ -100,10 +120,11 @@ type WithBannerActionVariants = Props extends ButtonProps ? Omit & { /** * Visual variant of the CTA, aligned with `Button`'s `variant` naming. - * - `"primary"` — filled accent gradient for the main action (default). - * - `"secondary"` — transparent with an accent-hued outline matching the banner. + * - `"secondary"` — accent-tinted fill with an accent hairline (default). + * - `"primary"` — filled accent gradient. Reserve for banners whose CTA is the + * single most important action on the page; it competes with page-level CTAs. * - `"ghost"` — text-only accent action with a faint accent-tinted hover. - * @default "primary" + * @default "secondary" */ variant?: BannerActionVariant; } @@ -115,15 +136,20 @@ export type BannerActionProps = WithBannerActionVariants; * A banner CTA built on Kumo's `Button`. It inherits Button's sizing, interaction, * loading, and accessibility behavior while supplying banner-specific accent styles. * + * Defaults to the quiet `"secondary"` treatment so the CTA stays subordinate to the + * banner's message; opt into `variant="primary"` when the action is the page's most + * important one. + * * @example * ```tsx * Retry + * Upgrade * } aria-label="Dismiss" /> * ``` */ export const BannerAction = forwardRef( function BannerAction( - { variant = "primary", className, style, ...props }, + { variant = "secondary", className, style, ...props }, ref, ) { const banner = useContext(BannerActionContext); diff --git a/packages/kumo/src/components/banner/banner.test.tsx b/packages/kumo/src/components/banner/banner.test.tsx index 0faa017a28..a9771a72c8 100644 --- a/packages/kumo/src/components/banner/banner.test.tsx +++ b/packages/kumo/src/components/banner/banner.test.tsx @@ -32,7 +32,11 @@ describe("Banner", () => { Retry} + action={ + + Retry + + } />, ); @@ -50,6 +54,26 @@ describe("Banner", () => { expect(cta.parentElement?.className).toContain("gap-2"); }); + it("defaults Banner.Action to the quiet accent-tinted secondary treatment", () => { + render( + Retry} + />, + ); + + const cta = screen.getByTestId("cta"); + expect(cta.className).toContain("bg-kumo-danger/12"); + expect(cta.className).toContain("ring-kumo-danger/40"); + expect(cta.className).toContain("text-kumo-danger"); + // Must not fall back to the loud filled primary CTA. + expect(cta.className).not.toContain("text-white"); + expect( + cta.style.getPropertyValue("--kumo-button-emphasis-gradient-end"), + ).toBe(""); + }); + it("styles a ghost Banner.Action with accent text and a tinted hover", () => { render( { const cta = screen.getByTestId("cta"); expect(cta.className).toContain("fill-kumo-warning"); - expect(cta.className).toContain("hover:bg-kumo-warning/10"); + expect(cta.className).toContain("text-kumo-warning"); + expect(cta.className).toContain("not-disabled:hover:bg-kumo-warning/12"); + }); + + it("keeps a disabled Banner.Action's hover treatment inert and dims it", () => { + render( + + Retry + + } + />, + ); + + const cta = screen.getByTestId("cta"); + expect((cta as HTMLButtonElement).disabled).toBe(true); + // Button dims the whole chip, so the accent fill/ring recede with it. + expect(cta.className).toContain("opacity-50"); + expect(cta.className).toContain("cursor-not-allowed"); + // Hover colors are gated behind not-disabled: so they cannot fire here, and + // none of them carry `!important` that would beat disabled:text-kumo-subtle. + expect(cta.className).not.toMatch(/(^|\s)hover:/); + expect(cta.className).not.toContain("!"); }); - it("styles a secondary Banner.Action as an accent-hued outline", () => { + it("styles a secondary Banner.Action as an accent-tinted chip", () => { render( { ); const cta = screen.getByTestId("cta"); - // Transparent bg + accent-hued ring in the same hue as the error banner accent. - expect(cta.className).toContain("ring-kumo-danger/50"); - expect(cta.className).toContain("hover:bg-kumo-danger/10"); - expect(cta.className).toContain("hover:!ring-kumo-danger/50"); - expect(cta.className).toContain("hover:!text-inherit"); + // Accent fill + accent hairline in the same hue as the error banner accent. + expect(cta.className).toContain("bg-kumo-danger/12"); + expect(cta.className).toContain("ring-kumo-danger/40"); + expect(cta.className).toContain("not-disabled:hover:bg-kumo-danger/20"); + expect(cta.className).toContain("not-disabled:hover:ring-kumo-danger/60"); + expect(cta.className).toContain("not-disabled:hover:text-kumo-danger"); + // Button's outline hover defaults are deduped away by tailwind-merge. + expect(cta.className).not.toContain("text-kumo-strong"); + expect(cta.className).not.toContain("ring-kumo-focus/25"); + // Explicit bright text token, not the banner's dimmed inherited color. + expect(cta.className).toContain("text-kumo-danger"); + expect(cta.className).not.toContain("text-inherit"); // Not the filled primary CTA. - expect(cta.className).toContain("bg-transparent"); expect(cta.className).not.toContain("text-white"); expect( cta.style.getPropertyValue("--kumo-button-emphasis-gradient-end"), diff --git a/packages/kumo/src/components/banner/banner.tsx b/packages/kumo/src/components/banner/banner.tsx index 4e956c7c4f..9343002b26 100644 --- a/packages/kumo/src/components/banner/banner.tsx +++ b/packages/kumo/src/components/banner/banner.tsx @@ -345,7 +345,8 @@ BannerRoot.displayName = "Banner"; * Full-width message bar with an optional trailing CTA slot. * * `Banner.Action` is an accent-aware CTA button - * (`variant="primary" | "secondary" | "ghost"`). + * (`variant="secondary" | "primary" | "ghost"`, defaulting to the quiet + * `"secondary"` treatment). */ export const Banner = Object.assign(BannerRoot, { Action: BannerAction,