Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/banner/banner.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

.content {
padding: var(--reactist-spacing-large);
align-items: flex-start;
}

.title,
Expand Down Expand Up @@ -74,24 +75,29 @@

.copy {
padding: calc(var(--reactist-spacing-xsmall) / 2) 0;
flex: 1 1 auto;
}
.copy .inlineLink:first-of-type {
margin-left: var(--reactist-spacing-xsmall);
}
.actions {
align-self: center;
}

@container banner (width < 235px) {
.content,
.staticContent {
.content {
flex-direction: column;
align-items: flex-start;
}

.icon {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.topContent {
flex-direction: column;
align-items: stretch;
}
.icon:has(.closeButton:only-child) {
display: flex;
}
Expand All @@ -101,7 +107,9 @@
.icon .closeButton:only-child {
margin-left: auto;
}

.actions {
align-self: flex-start;
}
.actions .closeButton {
display: none;
}
Expand Down
141 changes: 141 additions & 0 deletions src/banner/banner.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Stack } from '../stack'
import { Banner } from './banner'
import { Button } from '../button'
import { PromoImage } from './story-promo-image'
import { Notice } from '../notice'
import { CheckboxField } from '../checkbox-field'

<Meta
title="Design system/Banner"
Expand Down Expand Up @@ -35,6 +37,10 @@ export function getButton(buttonText) {
)
}

export function getLongDescription() {
return 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Provident cumque recusandae quibusdam, veniam cum illo? Inventore, doloremque necessitatibus! Sequi porro alias mollitia, temporibus quidem, aut modi tempora placeat laborum eos sapiente necessitatibus autem ipsum officia rerum distinctio consectetur tenetur qui! Perspiciatis ab corporis, itaque alias ex optio voluptatum nulla consequatur aut explicabo dolorem rerum ratione magnam. Mollitia dignissimos et ad commodi quasi molestias fugiat repellendus, magni distinctio voluptate neque quos esse asperiores iure excepturi eligendi eaque veniam voluptas blanditiis temporibus, omnis laborum quidem autem totam. Iure, numquam. Totam facilis dolorum, consequatur, eligendi est dolores modi dolore maiores ipsum magnam a.'
}

export function PlaygroundTemplate({ type, title, description, action }) {
return (
<Stack space="large" maxWidth="medium">
Expand Down Expand Up @@ -168,6 +174,29 @@ export function BannerActionExamples({ theme }) {
}}
onClose={() => ({})}
/>
<Banner
type="neutral"
icon={<ArchiveIcon />}
title="This is a sample title"
description={getLongDescription()}
action={{
type: 'button',
label: 'Action',
variant: 'primary',
}}
/>
<Banner
type="neutral"
icon={<ArchiveIcon />}
title="This is a sample title"
description={getLongDescription()}
action={{
type: 'button',
label: 'Action',
variant: 'primary',
}}
onClose={() => ({})}
/>
</Stack>
</Stack>
)
Expand Down Expand Up @@ -230,6 +259,102 @@ export function BannerImageExamples({ theme }) {
)
}

export function BannerContentExamples({ theme }) {
return (
<Stack space="medium" align="start">
<Stack space="large">
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
>
<Text tone="secondary">Some extra content here</Text>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
action={getButton('Click me!')}
>
<Text tone="secondary">Some extra content here</Text>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
action={getButton('Click me!')}
onClose={() => ({})}
>
<Text tone="secondary">Some extra content here</Text>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
icon={<ArchiveIcon />}
>
<Text tone="secondary">Some extra content here</Text>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
inlineLinks={[{ label: 'Learn more', href: '#' }]}
icon={<ArchiveIcon />}
action={getButton('Click me!')}
>
<Text tone="secondary">Some extra content here</Text>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description="Here’s the message below the title, sometimes the copy spans over two lines."
inlineLinks={[{ label: 'Learn more', href: '#' }]}
icon={<ArchiveIcon />}
action={getButton('Click me!')}
>
<Stack space="large">
<Text tone="secondary">Some extra content here</Text>
</Stack>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description={getLongDescription()}
inlineLinks={[{ label: 'Learn more', href: '#' }]}
icon={<ArchiveIcon />}
action={getButton('Click me!')}
>
<Stack space="large">
<Text tone="secondary">{getLongDescription()}</Text>
<Notice tone="info">
<Text>Please check the box to continue.</Text>
</Notice>
<CheckboxField label="Check me!" />
</Stack>
</Banner>
<Banner
type="neutral"
title="This is a sample title"
description={getLongDescription()}
inlineLinks={[{ label: 'Learn more', href: '#' }]}
icon={<ArchiveIcon />}
action={getButton('Click me!')}
onClose={() => ({})}
>
<Stack space="large">
<Text tone="secondary">{getLongDescription()}</Text>
<Notice tone="info">
<Text>Please check the box to continue.</Text>
</Notice>
<CheckboxField label="Check me!" />
</Stack>
</Banner>
</Stack>
</Stack>
)
}

# Banner

A simple banner component meant to be used to _inform_ the user of promotional content, disclaimers, warnings, as well as success and error states.
Expand Down Expand Up @@ -340,6 +465,22 @@ Image banners do not feature icons but can include body text or a combination of
</Story>
</Canvas>

### Content

Banners can include extra content.

<Canvas>
<Story
name="Content"
parameters={{
docs: { source: { type: 'code' } },
chromatic: { disableSnapshot: false },
}}
>
<BannerContentExamples theme="light" />
</Story>
</Canvas>

## Props

<ArgsTable of={Banner} />
Expand Down
9 changes: 9 additions & 0 deletions src/banner/banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,13 @@ describe('Banner', () => {
const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('renders a banner with children', () => {
render(
<Banner type="info" description="Info">
<p>Child content</p>
</Banner>,
)
expect(screen.getByText('Child content')).toBeInTheDocument()
})
})
104 changes: 60 additions & 44 deletions src/banner/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type BaseBanner = {
id?: string
title?: React.ReactNode
description: Exclude<React.ReactNode, null | undefined | boolean>
children?: React.ReactNode
action?: Action | React.ReactNode
inlineLinks?: InlineLink[]
} & CloseButton
Expand Down Expand Up @@ -112,6 +113,7 @@ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(function Banner(
type,
title,
description,
children,
action,
icon,
image,
Expand Down Expand Up @@ -153,57 +155,71 @@ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(function Banner(
>
{image ? <Box className={styles.image}>{image}</Box> : null}

<Box className={styles.content} display="flex" gap="small" alignItems="center">
<Box className={styles.staticContent} display="flex" gap="small" flexGrow={1}>
<Box className={styles.icon}>
{type === 'neutral' ? icon : <BannerIcon type={type} />}
{closeButton}
</Box>
<Box className={styles.content} display="flex" gap="small">
<Box className={styles.icon}>
{type === 'neutral' ? icon : <BannerIcon type={type} />}
{closeButton}
</Box>

<Box className={styles.copy} display="flex" flexDirection="column">
{title ? (
<Box id={titleId} className={styles.title}>
{title}
</Box>
) : null}
<Box display="flex" flexDirection="column" gap="small" flexGrow={1}>
<Box
className={styles.topContent}
Copy link
Contributor

@gnapse gnapse Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason you need this class is for:

    .topContent {
        flex-direction: column;
        align-items: stretch;
    }

The first one, you can pass as a Box prop. The 2nd one, we do not support stretch yet on the alignItems prop. But maybe we should. Could you consider sneaking that change in this very PR, and then take advantage of it?

Then you'd be able to remove that topContent css class name, and do this here instead:

 <Box
-    className={styles.topContent}
+    flexDirection="column"
+    alignItems="stretch"

If now, I can do it later in a new PR.

Copy link
Contributor Author

@engfragui engfragui Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles that you're referring to need to be applied only if @container banner (width < 235px) (very very small view ports):

@container banner (width < 235px) {
    // ....

    .topContent {
        flex-direction: column;
        align-items: stretch;
    }

    // ....
}

to ensure that things "look good" even in that case:

Screenshot 2025-11-13 at 15 06 30

If I were to apply flexDirection and alignItems to the <Box> regardless of the view port width (which I think it's what you're saying), then it would look weird when the view port is regular/wide:

Screenshot 2025-11-13 at 15 08 58

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I missed that. In that case, forget what I said.

display="flex"
gap="small"
alignItems="flexStart"
>
<Box
id={descriptionId}
className={[styles.description, title ? styles.secondary : null]}
className={styles.copy}
display="flex"
flexDirection="column"
flexGrow={1}
>
{description}
{inlineLinks?.map(({ label, ...props }, index) => {
return (
<React.Fragment key={index}>
<TextLink
{...props}
exceptionallySetClassName={styles.inlineLink}
>
{label}
</TextLink>
{index < inlineLinks.length - 1 ? <span> · </span> : ''}
</React.Fragment>
)
})}
{title ? (
<Box id={titleId} className={styles.title}>
{title}
</Box>
) : null}
<Box
id={descriptionId}
className={[styles.description, title ? styles.secondary : null]}
>
{description}
{inlineLinks?.map(({ label, ...props }, index) => {
return (
<React.Fragment key={index}>
<TextLink
{...props}
exceptionallySetClassName={styles.inlineLink}
>
{label}
</TextLink>
{index < inlineLinks.length - 1 ? <span> · </span> : ''}
</React.Fragment>
)
})}
</Box>
</Box>
</Box>
</Box>

{action || closeButton ? (
<Box className={styles.actions} display="flex" gap="small">
{action ? (
isActionObject(action) ? (
action.type === 'button' ? (
<ActionButton {...action} />
) : (
<ActionLink {...action} />
)
) : (
action
)
{action || closeButton ? (
<Box className={styles.actions} display="flex" gap="small">
{action ? (
isActionObject(action) ? (
action.type === 'button' ? (
<ActionButton {...action} />
) : (
<ActionLink {...action} />
)
) : (
action
)
) : null}
{closeButton}
</Box>
) : null}
{closeButton}
</Box>
) : null}

{children}
</Box>
</Box>
</Box>
)
Expand Down