# Banner

> Banners are persistent inline messages with an optional icon, action, close button and details panel.

Source: /docs/components/banner

```tsx
import { Banner } from '@aksornproject/aksara-ui'
```

## Variants

`Banner` is a namespace. `Banner.Base` is the root, and `Banner.Icon`, `Banner.Content`, `Banner.Title`, `Banner.Description`, `Banner.Action` and `Banner.Panel` fill it.

`variant` sets the fill, the icon colour and the default `role`. Use `information`, `success`, `warning` and `error` for a status. `default` is a neutral grey and `brand` uses the accent colour, for messages that carry no status.

The Banner never picks a glyph. Pass one to `Banner.Icon` through `icon` or `children`, and it renders in a 24px slot in the status colour.

```tsx
import {
  IconBasicShapesCircle,
  IconInterfaceEssentialDeleteDisabled,
  IconInterfaceEssentialDoneCheck,
  IconInterfaceEssentialInfoInformationCircle,
  IconInterfaceEssentialWarning,
} from '@aksornproject/aksara-icons'
import { Banner, type BannerVariant } from '@aksornproject/aksara-ui'
import type { ReactNode } from 'react'

const messages: {
  variant: BannerVariant
  icon: ReactNode
  title: string
  description: string
}[] = [
  {
    variant: 'information',
    icon: <IconInterfaceEssentialInfoInformationCircle aria-hidden />,
    title: 'Term 2 timetable published',
    description: 'Check your classes before Monday.',
  },
  {
    variant: 'success',
    icon: <IconInterfaceEssentialDoneCheck aria-hidden />,
    title: 'Grades submitted',
    description: 'Parents can see them from tomorrow.',
  },
  {
    variant: 'warning',
    icon: <IconInterfaceEssentialWarning aria-hidden />,
    title: 'Storage almost full',
    description: 'You have used 4.6 GB of 5 GB.',
  },
  {
    variant: 'error',
    icon: <IconInterfaceEssentialDeleteDisabled aria-hidden />,
    title: 'Sync failed',
    description: 'Attendance from 9 May did not upload.',
  },
  {
    variant: 'default',
    icon: <IconBasicShapesCircle aria-hidden />,
    title: 'Maintenance on Saturday',
    description: 'The site is read-only from 22:00 to 23:00.',
  },
  {
    variant: 'brand',
    icon: <IconBasicShapesCircle aria-hidden />,
    title: 'New lesson library',
    description: 'Browse 300 ready-made lesson plans.',
  },
]

export default function BannerVariants() {
  return (
    <div className="flex w-full flex-col gap-spacing-12">
      {messages.map(({ variant, icon, title, description }) => (
        <Banner.Base key={variant} variant={variant} showCloseButton={false}>
          <Banner.Icon icon={icon} />
          <Banner.Content>
            <Banner.Title>{title}</Banner.Title>
            <Banner.Description>{description}</Banner.Description>
          </Banner.Content>
        </Banner.Base>
      ))}
    </div>
  )
}
```

## Container

`container="card"`, the default, has a 12px radius. `section` has square corners, for a full-width message at the top of a page. Both fill their parent.

```tsx
import { IconInterfaceEssentialInfoInformationCircle } from '@aksornproject/aksara-icons'
import { Banner } from '@aksornproject/aksara-ui'

export default function BannerSection() {
  return (
    <Banner.Base
      variant="information"
      container="section"
      showCloseButton={false}
    >
      <Banner.Icon
        icon={<IconInterfaceEssentialInfoInformationCircle aria-hidden />}
      />
      <Banner.Content>
        <Banner.Title>You are viewing last year's class</Banner.Title>
        <Banner.Description>
          Changes here do not affect this term.
        </Banner.Description>
      </Banner.Content>
    </Banner.Base>
  )
}
```

## Action

`Banner.Action` holds your controls beside the text. It wraps below the text when less than 240px remains. It does not restyle its children, so pass a Secondary XSmall Button in the status colour.

```tsx
import { IconInterfaceEssentialDeleteDisabled } from '@aksornproject/aksara-icons'
import { Banner, Button } from '@aksornproject/aksara-ui'

export default function BannerAction() {
  return (
    <Banner.Base variant="error" showCloseButton={false}>
      <Banner.Icon
        icon={<IconInterfaceEssentialDeleteDisabled aria-hidden />}
      />
      <Banner.Content>
        <Banner.Title>Sync failed</Banner.Title>
        <Banner.Description>
          Attendance from 9 May did not upload.
        </Banner.Description>
      </Banner.Content>
      <Banner.Action>
        <Button type="button" color="destructive" variant="secondary" size="xs">
          Retry
        </Button>
      </Banner.Action>
    </Banner.Base>
  )
}
```

## Close

The close button shows by default. A click calls `onClose` with no arguments. The Banner keeps no visibility state, so the app unmounts it. `showCloseButton={false}` removes the button.

`closeButtonLabel` renames it, "Close alert" by default. `customCloseIcon` replaces the X glyph and `closeButtonClassName` merges last into the button.

```tsx
import { IconInterfaceEssentialDoneCheck } from '@aksornproject/aksara-icons'
import { Banner, Button } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function BannerDismiss() {
  const [visible, setVisible] = useState(true)

  return (
    <div className="flex w-full flex-col items-center gap-spacing-12">
      {visible ? (
        <Banner.Base variant="success" onClose={() => setVisible(false)}>
          <Banner.Icon icon={<IconInterfaceEssentialDoneCheck aria-hidden />} />
          <Banner.Content>
            <Banner.Title>Grades submitted</Banner.Title>
            <Banner.Description>
              Parents can see them from tomorrow.
            </Banner.Description>
          </Banner.Content>
        </Banner.Base>
      ) : (
        <Button variant="secondary-accent" onClick={() => setVisible(true)}>
          Show the banner again
        </Button>
      )}
    </div>
  )
}
```

## Disabled and loading

`disabled` greys the title and the description and disables the close and expand buttons. Controls in `Banner.Action` keep their own disabled state, so disable them yourself.

`loading` replaces every child and both buttons with an `aria-hidden` skeleton and sets `aria-busy`.

```tsx
import { IconInterfaceEssentialInfoInformationCircle } from '@aksornproject/aksara-icons'
import { Banner } from '@aksornproject/aksara-ui'

export default function BannerStates() {
  return (
    <div className="flex w-full flex-col gap-spacing-12">
      <Banner.Base variant="information" disabled>
        <Banner.Icon
          icon={<IconInterfaceEssentialInfoInformationCircle aria-hidden />}
        />
        <Banner.Content>
          <Banner.Title>Enrolment is closed</Banner.Title>
          <Banner.Description>It opens again on 1 June.</Banner.Description>
        </Banner.Content>
      </Banner.Base>
      <Banner.Base variant="information" loading>
        <Banner.Icon
          icon={<IconInterfaceEssentialInfoInformationCircle aria-hidden />}
        />
        <Banner.Content>
          <Banner.Title>Enrolment is closed</Banner.Title>
          <Banner.Description>It opens again on 1 June.</Banner.Description>
        </Banner.Content>
      </Banner.Base>
    </div>
  )
}
```

## Details panel

A `Banner.Panel` adds an expand button to the header and opens below it at the full width. The panel is unstyled, so the app sets its padding, fill and border.

`expandButtonLabel` names the expand button, "Details" by default. `expanded`, `defaultExpanded` and `onExpandedChange` control it. Use one Panel per Banner.

```tsx
import { IconInterfaceEssentialInfoInformationCircle } from '@aksornproject/aksara-icons'
import { Banner, typography } from '@aksornproject/aksara-ui'

export default function BannerDisclosure() {
  return (
    <Banner.Base
      variant="information"
      showCloseButton={false}
      expandButtonLabel="Release notes"
    >
      <Banner.Icon
        icon={<IconInterfaceEssentialInfoInformationCircle aria-hidden />}
      />
      <Banner.Content>
        <Banner.Title>Update available</Banner.Title>
        <Banner.Description>Reload when you are ready.</Banner.Description>
      </Banner.Content>
      <Banner.Panel
        className={`flex flex-col gap-spacing-8 border-t-(length:--border-border-1) border-color-border bg-color-background-surface px-spacing-16 py-spacing-12 ${typography('body-regular')} text-color-text-primary`}
      >
        Lesson plans now sort by unit. Attendance exports include late arrivals.
      </Banner.Panel>
    </Banner.Base>
  )
}
```

## Accessibility

- `error` and `warning` default to `role="alert"`. The other variants use `role="status"`. A `role` prop overrides the default.
- The close and expand buttons are `type="button"`, so a Banner inside a form never submits it.
- The expand button carries `aria-expanded`.
- Give a custom icon accessible text when it adds information. Otherwise mark it `aria-hidden`.
- `Banner.Icon` and `Banner.Panel` must be direct children of `Banner.Base` or sit inside fragments. `Content` and `Action` can be wrapped in your own components.
- A `render` target must keep the root role and the disclosure attributes.

## API

### Banner.Base

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, CollapsibleRootState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: CollapsibleRootState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `disabled` | `boolean` | `false` | Whether the component should ignore user interaction. |
| `variant` | `"brand" \| "default" \| "error" \| "information" \| "success" \| "warning"` | `'default'` | Sets the fill, the icon colour and the default `role`. |
| `container` | `"card" \| "section"` | `'card'` | `card` has a 12px radius and `section` has none. |
| `className` | `string` |  | Merged last with `cn()`, so these classes win. |
| `showCloseButton` | `boolean` | `true` | Shows the close button. |
| `onClose` | `(() => void)` |  | Called with no arguments when the close button is clicked. The app unmounts the Banner. |
| `customCloseIcon` | `ReactNode` |  | Replaces the X glyph inside the close button. |
| `closeButtonClassName` | `string` |  | Classes merged last into the close button. |
| `closeButtonLabel` | `string` | `'Close alert'` | The accessible name of the close button. |
| `expandButtonLabel` | `string` | `'Details'` | The accessible name of the expand button. |
| `loading` | `boolean` | `false` | Replaces the children with an `aria-hidden` skeleton and sets `aria-busy`. |
| `expanded` | `boolean` |  | Whether the `Banner.Panel` is open, for controlled use. |
| `defaultExpanded` | `boolean` |  | Whether the `Banner.Panel` starts open, for uncontrolled use. |
| `onExpandedChange` | `((expanded: boolean) => void)` |  | Called with the new open state when the expand button toggles the Panel. |

### Banner.Icon

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `icon` | `ReactNode` | The glyph. Without it the Icon renders its `children`. |

### Banner.Content, Banner.Title, Banner.Description, Banner.Action

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, {}> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### Banner.Panel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, CollapsiblePanelState> \| React.ReactElement<unknown, string \| React.JSXElementConstructor<any>>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `React.CSSProperties \| ((state: CollapsiblePanelState) => React.CSSProperties \| undefined)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `hiddenUntilFound` | `boolean` | `false` | Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keepMounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. |
| `keepMounted` | `boolean` | `false` | Whether to keep the element in the DOM while the panel is hidden. This prop is ignored when `hiddenUntilFound` is used. |
| `className` | `string` |  | Classes for the panel. The Banner adds none. |