From c3242457d7ff0c4bf293fc090ac646e2b928d995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Wed, 1 Jul 2026 10:13:46 +0200 Subject: [PATCH 1/2] feat(circuit-ui): Callout component Related: https://sumup.slack.com/archives/CURHLN94K/p1782128993767939 --- .changeset/curvy-plums-call.md | 5 + .../circuit-ui/components/Callout/Callout.mdx | 43 ++++++++ .../components/Callout/Callout.module.css | 64 ++++++++++++ .../components/Callout/Callout.spec.tsx | 81 +++++++++++++++ .../components/Callout/Callout.stories.tsx | 71 ++++++++++++++ .../circuit-ui/components/Callout/Callout.tsx | 98 +++++++++++++++++++ .../circuit-ui/components/Callout/index.tsx | 17 ++++ packages/circuit-ui/index.ts | 5 + skills/circuit-ui/references/components.md | 1 + .../references/components/Callout.mdx | 43 ++++++++ 10 files changed, 428 insertions(+) create mode 100644 .changeset/curvy-plums-call.md create mode 100644 packages/circuit-ui/components/Callout/Callout.mdx create mode 100644 packages/circuit-ui/components/Callout/Callout.module.css create mode 100644 packages/circuit-ui/components/Callout/Callout.spec.tsx create mode 100644 packages/circuit-ui/components/Callout/Callout.stories.tsx create mode 100644 packages/circuit-ui/components/Callout/Callout.tsx create mode 100644 packages/circuit-ui/components/Callout/index.tsx create mode 100644 skills/circuit-ui/references/components/Callout.mdx diff --git a/.changeset/curvy-plums-call.md b/.changeset/curvy-plums-call.md new file mode 100644 index 0000000000..349a8f23ac --- /dev/null +++ b/.changeset/curvy-plums-call.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Added the experimental Callout component for static inline guidance or emphasis. diff --git a/packages/circuit-ui/components/Callout/Callout.mdx b/packages/circuit-ui/components/Callout/Callout.mdx new file mode 100644 index 0000000000..3b7a7e93d7 --- /dev/null +++ b/packages/circuit-ui/components/Callout/Callout.mdx @@ -0,0 +1,43 @@ +import { Meta, Status, Props, Story } from '../../../../.storybook/components'; +import * as Stories from './Callout.stories'; + + + +# Callout + + + +The `Callout` component renders static inline guidance or emphasis within the content flow. + + + + +## Component variations + +### Callout variants + + + +- Use the 'info' variant for informational, neutral messages. +- Use the 'success' variant for successful messages. +- Use the 'warning' variant for warning messages, and other information a user should be aware of. +- Use the 'danger' variant for error messages. Be descriptive and give users clear next steps. +- Use the 'promo' variant for promotional or upgrade-related messages. + +### Callouts with rich content + +The callout body supports arbitrary React nodes. + + + +## Accessibility + +### Best practices + +#### Place callouts close to their related content + +Callouts are part of the content flow, and should be placed near their related content. Make use of headings and semantic markup to programmatically group content into meaningful sections. + +#### Avoid dynamically rendering a callout + +The `Callout` component is meant for static content. If you need to render a message dynamically, you most likely want to use the [`NotificationInline`](Notifications/NotificationInline) component instead. diff --git a/packages/circuit-ui/components/Callout/Callout.module.css b/packages/circuit-ui/components/Callout/Callout.module.css new file mode 100644 index 0000000000..f4ae944136 --- /dev/null +++ b/packages/circuit-ui/components/Callout/Callout.module.css @@ -0,0 +1,64 @@ +.base { + display: flex; + flex-direction: row; + align-items: center; + padding: var(--cui-spacings-kilo) var(--cui-spacings-mega); + border-radius: var(--cui-border-radius-kilo); +} + +.icon { + position: relative; + flex-grow: 0; + flex-shrink: 0; + align-self: flex-start; + line-height: 0; +} + +.content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: var(--cui-spacings-mega); +} + +/* Variants */ + +.info { + background-color: var(--cui-bg-subtle); +} + +.info .icon { + color: var(--cui-fg-accent); +} + +.success { + background-color: var(--cui-bg-success); +} + +.success .icon { + color: var(--cui-fg-success); +} + +.warning { + background-color: var(--cui-bg-warning); +} + +.warning .icon { + color: var(--cui-fg-warning); +} + +.danger { + background-color: var(--cui-bg-danger); +} + +.danger .icon { + color: var(--cui-fg-danger); +} + +.promo { + background-color: var(--cui-bg-promo); +} + +.promo .icon { + color: var(--cui-fg-promo); +} diff --git a/packages/circuit-ui/components/Callout/Callout.spec.tsx b/packages/circuit-ui/components/Callout/Callout.spec.tsx new file mode 100644 index 0000000000..931c2d84d9 --- /dev/null +++ b/packages/circuit-ui/components/Callout/Callout.spec.tsx @@ -0,0 +1,81 @@ +/** + * Copyright 2026, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRef } from 'react'; +import { describe, expect, it } from 'vitest'; + +import type { IconProps } from '@sumup-oss/icons'; + +import { axe, render, screen } from '../../util/test-utils.js'; + +import { Callout, type CalloutProps } from './Callout.js'; + +describe('Callout', () => { + const renderCallout = (props: CalloutProps) => render(); + + const baseProps: CalloutProps = { + body: 'This is a callout', + }; + + it('should render the callout', () => { + renderCallout(baseProps); + + expect(screen.getByText('This is a callout')).toBeVisible(); + }); + + it.each([ + 'info', + 'success', + 'warning', + 'danger', + 'promo', + ] as const)('should render the %s variant', (variant) => { + const { container } = renderCallout({ ...baseProps, variant }); + + expect(container.firstElementChild?.className).toContain(`_${variant}_`); + }); + + it('should render a custom icon', () => { + const CustomIcon = (props: IconProps) => ( + + ); + + renderCallout({ ...baseProps, icon: CustomIcon }); + + const icon = screen.getByTestId('custom-icon'); + + expect(icon).toBeInTheDocument(); + }); + + it('should render the icon label visually hidden', () => { + renderCallout({ ...baseProps, iconLabel: 'Information' }); + + expect(screen.getByText('Information')).toBeInTheDocument(); + }); + + it('should forward a ref', () => { + const ref = createRef(); + const { container } = render(); + + expect(ref.current).toBe(container.firstChild); + }); + + it('should have no accessibility violations', async () => { + const { container } = renderCallout(baseProps); + const actual = await axe(container); + + expect(actual).toHaveNoViolations(); + }); +}); diff --git a/packages/circuit-ui/components/Callout/Callout.stories.tsx b/packages/circuit-ui/components/Callout/Callout.stories.tsx new file mode 100644 index 0000000000..6c18217abd --- /dev/null +++ b/packages/circuit-ui/components/Callout/Callout.stories.tsx @@ -0,0 +1,71 @@ +/** + * Copyright 2026, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Stack } from '../../../../.storybook/components/index.js'; +import { Anchor } from '../Anchor/Anchor.js'; +import { List } from '../List/List.js'; + +import { Callout, type CalloutProps } from './Callout.js'; + +export default { + title: 'Components/Callout', + component: Callout, + tags: ['status:experimental'], +}; + +const variants = ['info', 'success', 'warning', 'danger', 'promo'] as const; + +export const Base = (args: CalloutProps) => ; + +Base.args = { + body: 'Callout is a newly available component for static inline guidance or emphasis.', + variant: 'promo', +} as CalloutProps; + +Base.parameters = { + chromatic: { + // covered in the Variants story + disableSnapshot: true, + }, +}; + +export const Variants = (args: CalloutProps) => ( + + {variants.map((variant) => ( + + ))} + +); + +Variants.args = { + body: 'This is a callout message', +} as CalloutProps; + +export const WithRichContent = (args: CalloutProps) => ; + +WithRichContent.args = { + body: ( +
+ Callouts can include{' '} + links to related content and + lists: + +
  • Keep the content static.
  • +
  • Place it near the related interface.
  • +
    +
    + ), + variant: 'promo', +} as CalloutProps; diff --git a/packages/circuit-ui/components/Callout/Callout.tsx b/packages/circuit-ui/components/Callout/Callout.tsx new file mode 100644 index 0000000000..18e1a3bbfa --- /dev/null +++ b/packages/circuit-ui/components/Callout/Callout.tsx @@ -0,0 +1,98 @@ +/** + * Copyright 2026, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'; + +import { + Alert, + Confirm, + Info, + Notify, + Sparkles, + type IconComponentType, +} from '@sumup-oss/icons'; + +import { Body } from '../Body/index.js'; +import { clsx } from '../../styles/clsx.js'; +import { utilClasses } from '../../styles/utility.js'; + +import classes from './Callout.module.css'; + +export type CalloutVariant = + | 'info' + | 'success' + | 'warning' + | 'danger' + | 'promo'; + +const CALLOUT_ICONS: Record> = { + info: Info, + success: Confirm, + warning: Notify, + danger: Alert, + promo: Sparkles, +}; + +export interface CalloutProps extends HTMLAttributes { + /** + * The callout's variant. + * @default 'info' + */ + variant?: CalloutVariant; + /** + * The callout's body content. + */ + body: ReactNode; + /** + * The icon displayed next to the body. + */ + icon?: IconComponentType<'24'>; + /** + * A text replacement for the icon in the context of the callout, if its body + * copy isn't self-explanatory. Defaults to an empty string. + */ + iconLabel?: string; +} + +/** + * The `Callout` component renders static inline guidance or emphasis within + * the content flow. + */ +export const Callout = forwardRef( + ( + { variant = 'info', body, icon, iconLabel = '', className, ...props }, + ref, + ) => { + const Icon = icon || CALLOUT_ICONS[variant]; + + return ( +
    +
    +
    + {iconLabel} + + {body} + +
    + ); + }, +); + +Callout.displayName = 'Callout'; diff --git a/packages/circuit-ui/components/Callout/index.tsx b/packages/circuit-ui/components/Callout/index.tsx new file mode 100644 index 0000000000..42957dee56 --- /dev/null +++ b/packages/circuit-ui/components/Callout/index.tsx @@ -0,0 +1,17 @@ +/** + * Copyright 2026, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Callout } from './Callout.js'; +export type { CalloutProps, CalloutVariant } from './Callout.js'; diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index e989748ec9..124005d0bf 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -86,6 +86,11 @@ export type { SelectorGroupProps } from './components/SelectorGroup/index.js'; export type { ClickEvent } from './types/events.js'; // Notifications +export { Callout } from './components/Callout/index.js'; +export type { + CalloutProps, + CalloutVariant, +} from './components/Callout/index.js'; export { NotificationBanner } from './components/NotificationBanner/index.js'; export type { NotificationBannerProps } from './components/NotificationBanner/index.js'; export { NotificationFullscreen } from './components/NotificationFullscreen/index.js'; diff --git a/skills/circuit-ui/references/components.md b/skills/circuit-ui/references/components.md index 8773e01db1..8fc429f231 100644 --- a/skills/circuit-ui/references/components.md +++ b/skills/circuit-ui/references/components.md @@ -91,6 +91,7 @@ | Component | Status | Package | Source export | Usage reference | | --- | --- | --- | --- | --- | +| `Callout` | `experimental` | `@sumup-oss/circuit-ui` | `./components/Callout/index.js` | [Read MDX reference](components/Callout.mdx) | | `NotificationBanner` | `stable` | `@sumup-oss/circuit-ui` | `./components/NotificationBanner/index.js` | [Read MDX reference](components/NotificationBanner.mdx) | | `NotificationFullscreen` | `stable` | `@sumup-oss/circuit-ui` | `./components/NotificationFullscreen/index.js` | [Read MDX reference](components/NotificationFullscreen.mdx) | | `NotificationInline` | `stable` | `@sumup-oss/circuit-ui` | `./components/NotificationInline/index.js` | [Read MDX reference](components/NotificationInline.mdx) | diff --git a/skills/circuit-ui/references/components/Callout.mdx b/skills/circuit-ui/references/components/Callout.mdx new file mode 100644 index 0000000000..3b7a7e93d7 --- /dev/null +++ b/skills/circuit-ui/references/components/Callout.mdx @@ -0,0 +1,43 @@ +import { Meta, Status, Props, Story } from '../../../../.storybook/components'; +import * as Stories from './Callout.stories'; + + + +# Callout + + + +The `Callout` component renders static inline guidance or emphasis within the content flow. + + + + +## Component variations + +### Callout variants + + + +- Use the 'info' variant for informational, neutral messages. +- Use the 'success' variant for successful messages. +- Use the 'warning' variant for warning messages, and other information a user should be aware of. +- Use the 'danger' variant for error messages. Be descriptive and give users clear next steps. +- Use the 'promo' variant for promotional or upgrade-related messages. + +### Callouts with rich content + +The callout body supports arbitrary React nodes. + + + +## Accessibility + +### Best practices + +#### Place callouts close to their related content + +Callouts are part of the content flow, and should be placed near their related content. Make use of headings and semantic markup to programmatically group content into meaningful sections. + +#### Avoid dynamically rendering a callout + +The `Callout` component is meant for static content. If you need to render a message dynamically, you most likely want to use the [`NotificationInline`](Notifications/NotificationInline) component instead. From 48a0a6890c79f8f217b467b164d31306b7940347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 3 Jul 2026 16:20:42 +0200 Subject: [PATCH 2/2] chore: address code review comments --- .../circuit-ui/components/Callout/Callout.mdx | 16 +++++++++++----- .../components/Callout/Callout.module.css | 4 ++++ .../components/Callout/Callout.stories.tsx | 10 ++-------- .../circuit-ui/components/Callout/Callout.tsx | 5 +---- .../circuit-ui/references/components/Callout.mdx | 16 +++++++++++----- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/circuit-ui/components/Callout/Callout.mdx b/packages/circuit-ui/components/Callout/Callout.mdx index 3b7a7e93d7..d3f95cafaa 100644 --- a/packages/circuit-ui/components/Callout/Callout.mdx +++ b/packages/circuit-ui/components/Callout/Callout.mdx @@ -7,14 +7,20 @@ import * as Stories from './Callout.stories'; -The `Callout` component renders static inline guidance or emphasis within the content flow. +The `Callout` component renders static guidance or emphasis content within the content flow. -## Component variations +## When to use it -### Callout variants +Use callouts to highlight persistent guidance, contextual tips, or extra information that belongs next to related content. + +Keep callout copy concise and static. For semantic feedback to user actions, use [`NotificationInline`](Notifications/NotificationInline). For dismissible content or content with a call to action, use [`NotificationBanner`](Notifications/NotificationBanner). + +## How to use it + +### Variants @@ -24,9 +30,9 @@ The `Callout` component renders static inline guidance or emphasis within the co - Use the 'danger' variant for error messages. Be descriptive and give users clear next steps. - Use the 'promo' variant for promotional or upgrade-related messages. -### Callouts with rich content +### Links -The callout body supports arbitrary React nodes. +The callout body supports links to related content. Avoid placing other interactive content inside a callout. diff --git a/packages/circuit-ui/components/Callout/Callout.module.css b/packages/circuit-ui/components/Callout/Callout.module.css index f4ae944136..bf14a68e3a 100644 --- a/packages/circuit-ui/components/Callout/Callout.module.css +++ b/packages/circuit-ui/components/Callout/Callout.module.css @@ -19,6 +19,10 @@ flex-direction: column; align-items: flex-start; padding-left: var(--cui-spacings-mega); + font-size: var(--cui-body-m-font-size); + font-weight: var(--cui-font-weight-regular); + line-height: var(--cui-body-m-line-height); + letter-spacing: var(--cui-letter-spacing); } /* Variants */ diff --git a/packages/circuit-ui/components/Callout/Callout.stories.tsx b/packages/circuit-ui/components/Callout/Callout.stories.tsx index 6c18217abd..12eaa5c1d9 100644 --- a/packages/circuit-ui/components/Callout/Callout.stories.tsx +++ b/packages/circuit-ui/components/Callout/Callout.stories.tsx @@ -15,12 +15,11 @@ import { Stack } from '../../../../.storybook/components/index.js'; import { Anchor } from '../Anchor/Anchor.js'; -import { List } from '../List/List.js'; import { Callout, type CalloutProps } from './Callout.js'; export default { - title: 'Components/Callout', + title: 'Notifications/Callout', component: Callout, tags: ['status:experimental'], }; @@ -59,12 +58,7 @@ WithRichContent.args = { body: (
    Callouts can include{' '} - links to related content and - lists: - -
  • Keep the content static.
  • -
  • Place it near the related interface.
  • -
    + links to related content.
    ), variant: 'promo', diff --git a/packages/circuit-ui/components/Callout/Callout.tsx b/packages/circuit-ui/components/Callout/Callout.tsx index 18e1a3bbfa..6ec12bf53d 100644 --- a/packages/circuit-ui/components/Callout/Callout.tsx +++ b/packages/circuit-ui/components/Callout/Callout.tsx @@ -24,7 +24,6 @@ import { type IconComponentType, } from '@sumup-oss/icons'; -import { Body } from '../Body/index.js'; import { clsx } from '../../styles/clsx.js'; import { utilClasses } from '../../styles/utility.js'; @@ -87,9 +86,7 @@ export const Callout = forwardRef(