diff --git a/e2e/base/components/Banner.spec.tsx b/e2e/base/components/Banner.spec.tsx
new file mode 100644
index 000000000..37211b598
--- /dev/null
+++ b/e2e/base/components/Banner.spec.tsx
@@ -0,0 +1,69 @@
+import { StoryBox } from '../../../.storybook/components/StoryBox'
+import { Level } from '../../../src'
+import { _Banner as BannerStory } from '../../../stories/components/Banner.stories'
+import { mountAndWait } from '../utils'
+
+context(`Story`, () => {
+ describe('default visibility', () => {
+ it('should not show the banner when isHiddenByDefault is true', () => {
+ mountAndWait(
+
+
+ some text
+
+
+ )
+ cy.get('.banner').should('not.be.visible')
+ })
+ it('should show the banner when isHiddenByDefault is false', () => {
+ mountAndWait(
+
+
+ some text
+
+
+ )
+ cy.get('.banner').should('be.visible')
+ })
+ it('should show the banner when isHiddenByDefault is undefined', () => {
+ mountAndWait(
+
+
+ some text
+
+
+ )
+ cy.get('.banner').should('be.visible')
+ })
+ })
+ describe('closable version', () => {
+ it('should disappear when the action button is clicked', () => {
+ mountAndWait(
+
+
+ some text
+
+
+ )
+ cy.get('.banner-button').click()
+ cy.get('.banner').should('not.be.visible')
+ })
+ })
+ describe('collapsible version', () => {
+ it('should collapse when the action button is clicked', () => {
+ mountAndWait(
+
+
+ some text
+
+
+ )
+ // check it's fully opened
+ cy.get('.banner').invoke('outerHeight').should('be.gt', 10)
+ // click on the hide button
+ cy.get('.banner-button').click()
+ // check it's shrunk
+ cy.get('.banner').invoke('outerHeight').should('be.equal', 10)
+ })
+ })
+})
diff --git a/src/components/Banner/__tests__/getBannerPalette.test.ts b/src/components/Banner/__tests__/getBannerPalette.test.ts
new file mode 100644
index 000000000..c2dafb130
--- /dev/null
+++ b/src/components/Banner/__tests__/getBannerPalette.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, test } from '@jest/globals'
+
+import { Level } from '../../../constants'
+import { THEME } from '../../../theme'
+import { getBannerPalette } from '../utils'
+
+describe('getBannerPalette', () => {
+ test('should return a red palette for the Error level', () => {
+ expect(getBannerPalette(Level.ERROR)).toStrictEqual({
+ backgroundColor: THEME.color.maximumRed15,
+ borderColor: THEME.color.maximumRed,
+ color: THEME.color.maximumRed
+ })
+ })
+ test('should return a yellow palette for the Warning level', () => {
+ expect(getBannerPalette(Level.WARNING)).toStrictEqual({
+ backgroundColor: THEME.color.goldenPoppy25,
+ borderColor: THEME.color.goldenPoppy,
+ color: THEME.color.charcoal
+ })
+ })
+ test('should return a green palette for the success level', () => {
+ expect(getBannerPalette(Level.SUCCESS)).toStrictEqual({
+ backgroundColor: THEME.color.mediumSeaGreen25,
+ borderColor: THEME.color.mediumSeaGreen,
+ color: THEME.color.mediumSeaGreen
+ })
+ })
+})
diff --git a/src/components/Banner/index.tsx b/src/components/Banner/index.tsx
new file mode 100644
index 000000000..9fb7bb586
--- /dev/null
+++ b/src/components/Banner/index.tsx
@@ -0,0 +1,122 @@
+import { IconButton } from '@elements/IconButton'
+import { LinkButton } from '@elements/LinkButton'
+import { isString } from 'lodash'
+import { type ReactNode, useState } from 'react'
+import styled from 'styled-components'
+
+import { getBannerPalette } from './utils'
+import { Accent, Icon, Level, Size } from '../../constants'
+
+export type BannerProps = {
+ children: string | ReactNode
+ isClosable: boolean
+ isCollapsible: boolean
+ isHiddenByDefault: boolean | undefined
+ level: Level
+ top: string
+}
+
+interface WrapperProps {
+ $isCollapsed: boolean
+ $isCollapsible: boolean
+ $isHidden: boolean
+ $level: Level
+ $top: string
+}
+const Wrapper = styled.div`
+ display: ${(p: WrapperProps) => (p.$isHidden ? 'none' : 'flex')};
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ position: absolute;
+ background-color: ${(p: WrapperProps) => getBannerPalette(p.$level).backgroundColor};
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ padding: 0 2rem;
+ top: ${(p: WrapperProps) => `${p.$top}`};
+ z-index: 1000;
+ height: ${(p: WrapperProps) => (!p.$isHidden && p.$isCollapsed ? '10px' : '50px')};
+ border-bottom: ${({ $isCollapsible, $level }: WrapperProps) =>
+ $isCollapsible ? `4px solid ${getBannerPalette($level).borderColor}` : 'none'};
+ box-shadow: ${({ $isCollapsible }: WrapperProps) => ($isCollapsible ? 'none' : '0px 3px 4px #7077854D')};
+ transition: height 0.3s ease;
+`
+
+interface ContentWrapperProps {
+ $level: Level
+}
+const ContentWrapper = styled.div`
+ color: ${(p: ContentWrapperProps) => getBannerPalette(p.$level).color};
+ align-self: center;
+ flex-grow: 2;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 500;
+`
+
+const ButtonWrapper = styled.div`
+ align-self: center;
+`
+
+function Banner({ children, isClosable, isCollapsible, isHiddenByDefault, level, top }: Readonly) {
+ const [isHidden, setIsHidden] = useState(!!isHiddenByDefault)
+ const [isCollapsed, setIsCollapsed] = useState(false)
+ const [isCollapsing, setIsCollapsing] = useState(false)
+ const [hasCollapsed, setHasCollapsed] = useState(false)
+
+ const enterHover = (): void => {
+ if (!isHidden && isCollapsed && !isCollapsing) {
+ setIsCollapsed(false)
+ }
+ setIsCollapsing(false)
+ }
+ const leaveHover = (): void => {
+ if (!isHidden && hasCollapsed) {
+ setIsCollapsed(true)
+ }
+ }
+
+ const onClickAction = (): void => {
+ if (isClosable) {
+ setIsHidden(true)
+ } else if (isCollapsible) {
+ setIsCollapsing(true)
+ setIsCollapsed(true)
+ setHasCollapsed(true)
+ }
+ }
+
+ return (
+
+ {!isHidden && !isCollapsed && (
+ <>
+ {isString(children) ? {children}
: <>{children}>}
+ onClickAction()}
+ title={isClosable ? 'fermer' : 'masquer'}
+ >
+ {isClosable && (
+
+ )}
+ {!isClosable && isCollapsible && Masquer}
+
+ >
+ )}
+
+ )
+}
+
+Banner.displayName = 'Banner'
+
+export { Banner }
diff --git a/src/components/Banner/utils.ts b/src/components/Banner/utils.ts
new file mode 100644
index 000000000..e93214ad2
--- /dev/null
+++ b/src/components/Banner/utils.ts
@@ -0,0 +1,25 @@
+import { Level } from '../../constants'
+import { THEME } from '../../theme'
+
+export const getBannerPalette = (level: Level) => {
+ if (level === Level.ERROR) {
+ return {
+ backgroundColor: THEME.color.maximumRed15,
+ borderColor: THEME.color.maximumRed,
+ color: THEME.color.maximumRed
+ }
+ }
+ if (level === Level.WARNING) {
+ return {
+ backgroundColor: THEME.color.goldenPoppy25,
+ borderColor: THEME.color.goldenPoppy,
+ color: THEME.color.charcoal
+ }
+ }
+
+ return {
+ backgroundColor: THEME.color.mediumSeaGreen25,
+ borderColor: THEME.color.mediumSeaGreen,
+ color: THEME.color.mediumSeaGreen
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 0e01f3941..4f021cd1e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -25,6 +25,7 @@ export { SideMenu } from './components/SideMenu'
export { SingleTag } from './elements/SingleTag'
export { MapMenuDialog } from './components/MapMenuDialog'
export { Message } from './components/Message'
+export { Banner } from './components/Banner'
export { Button } from './elements/Button'
export { Field } from './elements/Field'
@@ -198,6 +199,7 @@ export type { NotifierProps } from './components/Notifier'
export type { SideMenuProps } from './components/SideMenu'
export type { SingleTagProps } from './elements/SingleTag'
export type { MessageProps } from './components/Message'
+export type { BannerProps } from './components/Banner'
export type { ButtonProps } from './elements/Button'
export type { FieldProps } from './elements/Field'
diff --git a/stories/components/Banner.stories.tsx b/stories/components/Banner.stories.tsx
new file mode 100644
index 000000000..13bfb2538
--- /dev/null
+++ b/stories/components/Banner.stories.tsx
@@ -0,0 +1,53 @@
+import { ARG_TYPE, META_DEFAULTS } from '../../.storybook/constants'
+import { generateStoryDecorator } from '../../.storybook/utils/generateStoryDecorator'
+import { Level, THEME } from '../../src'
+import { Banner } from '../../src/components/Banner'
+
+import type { BannerProps } from '../../src/components/Banner'
+import type { Meta } from '@storybook/react'
+
+/* eslint-disable sort-keys-fix/sort-keys-fix */
+const meta: Meta = {
+ ...META_DEFAULTS,
+
+ title: 'Components/Banner',
+ component: Banner,
+
+ argTypes: {
+ isHiddenByDefault: ARG_TYPE.OPTIONAL_BOOLEAN,
+ level: ARG_TYPE.OPTIONAL_LEVEL,
+ isCollapsible: ARG_TYPE.BOOLEAN,
+ isClosable: ARG_TYPE.BOOLEAN
+ },
+
+ args: {
+ isHiddenByDefault: false,
+ isCollapsible: true,
+ isClosable: false,
+ level: Level.SUCCESS,
+ children: 'This is the content of the banner',
+ top: '76px'
+ },
+
+ decorators: [generateStoryDecorator()]
+}
+/* eslint-enable sort-keys-fix/sort-keys-fix */
+
+export default meta
+
+export function _Banner(props: BannerProps) {
+ return (
+
+
+
This is a header
+
+
+
+ )
+}