Stage Banner
A full-width announcement strip with a status dot, optional action button and dismiss control.
A full-width announcement strip with a status dot, optional action button and dismiss control. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-stage-bannerimport {
StageBanner,
StageBannerAction,
StageBannerClose,
StageBannerDot,
} from "@/registry/bases/newt/ui/stage-banner"
export default function StageBannerDemo() {
return (
<div className="flex w-full max-w-md flex-col gap-4">
<StageBanner>
<StageBannerDot />
Announcement text goes here.
<StageBannerAction>Action</StageBannerAction>
</StageBanner>
<StageBanner variant="neutral">
<StageBannerDot status="online" />
Status message goes here.
<StageBannerClose className="text-newt-text-muted hover:text-newt-text-primary" />
</StageBanner>
</div>
)
}<script setup lang="ts">
import {
StageBanner,
StageBannerAction,
StageBannerClose,
StageBannerDot,
} from "~~/registry/bases/newt/ui/stage-banner"
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<StageBanner>
<StageBannerDot />
Announcement text goes here.
<StageBannerAction>Action</StageBannerAction>
</StageBanner>
<StageBanner variant="neutral">
<StageBannerDot status="online" />
Status message goes here.
<StageBannerClose
class="text-newt-text-muted hover:text-newt-text-primary"
/>
</StageBanner>
</div>
</template>Installation
pnpm dlx newtui@latest add stage-bannernpx newtui@latest add stage-banneryarn dlx newtui@latest add stage-bannerbunx newtui@latest add stage-bannerInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityCopy the source into your project
"use client"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
const stageBannerVariants = cva(
"group flex items-center gap-3 px-4 py-2.5 text-[13px] font-medium",
{
variants: {
variant: {
brand: "bg-newt-brand text-white",
neutral:
"border border-newt-border bg-newt-bg-elevated text-newt-text-primary",
},
},
defaultVariants: {
variant: "brand",
},
}
)
export interface StageBannerProps
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof stageBannerVariants> {
/** Controlled visibility; when omitted the banner manages its own state. */
open?: boolean
/** Fired when the banner is dismissed via StageBannerClose. */
onOpenChange?: (open: boolean) => void
}
type StageBannerContextValue = { close: () => void }
const StageBannerContext = React.createContext<StageBannerContextValue>({
close: () => {},
})
const StageBanner = React.forwardRef<HTMLDivElement, StageBannerProps>(
({ className, variant, open, onOpenChange, children, ...props }, ref) => {
const [internalOpen, setInternalOpen] = React.useState(true)
const isControlled = open !== undefined
const isOpen = isControlled ? open : internalOpen
const close = React.useCallback(() => {
if (!isControlled) setInternalOpen(false)
onOpenChange?.(false)
}, [isControlled, onOpenChange])
if (!isOpen) return null
return (
<StageBannerContext.Provider value={{ close }}>
<div
ref={ref}
data-variant={variant ?? "brand"}
className={cn(stageBannerVariants({ variant }), className)}
{...props}
>
{children}
</div>
</StageBannerContext.Provider>
)
}
)
StageBanner.displayName = "StageBanner"
const stageBannerDotVariants = cva("h-2 w-2 shrink-0 rounded-full", {
variants: {
status: {
white: "bg-white",
online: "bg-newt-online",
idle: "bg-newt-idle",
dnd: "bg-newt-dnd",
offline: "bg-newt-offline",
},
},
defaultVariants: {
status: "white",
},
})
export interface StageBannerDotProps
extends
React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof stageBannerDotVariants> {}
const StageBannerDot = React.forwardRef<HTMLSpanElement, StageBannerDotProps>(
({ className, status, ...props }, ref) => (
<span
ref={ref}
aria-hidden="true"
className={cn(stageBannerDotVariants({ status }), className)}
{...props}
/>
)
)
StageBannerDot.displayName = "StageBannerDot"
const StageBannerAction = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, type = "button", ...props }, ref) => (
<button
ref={ref}
type={type}
className={cn(
"ms-auto shrink-0 cursor-pointer rounded-sm border-0 bg-black/20 px-2.5 py-1 text-xs font-semibold text-white transition-colors duration-fast ease-newt hover:bg-black/[0.32]",
className
)}
{...props}
/>
))
StageBannerAction.displayName = "StageBannerAction"
const StageBannerClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, onClick, children, type = "button", ...props }, ref) => {
const { close } = React.useContext(StageBannerContext)
return (
<button
ref={ref}
type={type}
aria-label="Dismiss"
className={cn(
"ms-auto shrink-0 cursor-pointer border-0 bg-transparent p-0 text-base leading-none text-white/80 transition-colors duration-fast ease-newt hover:text-white",
"group-data-[variant=neutral]:text-newt-text-muted group-data-[variant=neutral]:hover:text-newt-text-primary",
className
)}
onClick={(e) => {
onClick?.(e)
if (!e.defaultPrevented) close()
}}
{...props}
>
{children ?? "×"}
</button>
)
})
StageBannerClose.displayName = "StageBannerClose"
export {
StageBanner,
StageBannerDot,
StageBannerAction,
StageBannerClose,
stageBannerVariants,
stageBannerDotVariants,
}<script setup lang="ts">
import { computed, provide, ref, type HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import {
STAGE_BANNER_INJECTION_KEY,
stageBannerVariants,
type StageBannerVariants,
} from "."
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
variant?: StageBannerVariants["variant"]
open?: boolean
}>(),
{ variant: "brand", open: undefined }
)
const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
const internalOpen = ref(true)
const isControlled = computed(() => props.open !== undefined)
const isOpen = computed(() =>
isControlled.value ? props.open : internalOpen.value
)
function close() {
if (!isControlled.value) internalOpen.value = false
emit("update:open", false)
}
provide(STAGE_BANNER_INJECTION_KEY, { close })
</script>
<template>
<div
v-if="isOpen"
:data-variant="props.variant"
:class="cn(stageBannerVariants({ variant: props.variant }), props.class)"
>
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { stageBannerDotVariants, type StageBannerDotVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
status?: StageBannerDotVariants["status"]
}>()
</script>
<template>
<span
aria-hidden="true"
:class="cn(stageBannerDotVariants({ status: props.status }), props.class)"
/>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<button
type="button"
:class="
cn(
'ms-auto shrink-0 cursor-pointer rounded-sm border-0 bg-black/20 px-2.5 py-1 text-xs font-semibold text-white transition-colors duration-fast ease-newt hover:bg-black/[0.32]',
props.class
)
"
>
<slot />
</button>
</template><script setup lang="ts">
import { inject, type HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { STAGE_BANNER_INJECTION_KEY } from "."
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const ctx = inject(STAGE_BANNER_INJECTION_KEY, null)
</script>
<template>
<button
type="button"
aria-label="Dismiss"
:class="
cn(
'ms-auto shrink-0 cursor-pointer border-0 bg-transparent p-0 text-base leading-none text-white/80 transition-colors duration-fast ease-newt hover:text-white group-data-[variant=neutral]:text-newt-text-muted group-data-[variant=neutral]:hover:text-newt-text-primary',
props.class
)
"
@click="ctx?.close()"
>
<slot>×</slot>
</button>
</template>import { cva, type VariantProps } from "class-variance-authority"
import type { InjectionKey } from "vue"
export { default as StageBanner } from "./StageBanner.vue"
export { default as StageBannerDot } from "./StageBannerDot.vue"
export { default as StageBannerAction } from "./StageBannerAction.vue"
export { default as StageBannerClose } from "./StageBannerClose.vue"
export const stageBannerVariants = cva(
"group flex items-center gap-3 px-4 py-2.5 text-[13px] font-medium",
{
variants: {
variant: {
brand: "bg-newt-brand text-white",
neutral:
"border border-newt-border bg-newt-bg-elevated text-newt-text-primary",
},
},
defaultVariants: {
variant: "brand",
},
}
)
export const stageBannerDotVariants = cva("h-2 w-2 shrink-0 rounded-full", {
variants: {
status: {
white: "bg-white",
online: "bg-newt-online",
idle: "bg-newt-idle",
dnd: "bg-newt-dnd",
offline: "bg-newt-offline",
},
},
defaultVariants: {
status: "white",
},
})
export type StageBannerVariants = VariantProps<typeof stageBannerVariants>
export type StageBannerDotVariants = VariantProps<typeof stageBannerDotVariants>
export interface StageBannerContext {
close: () => void
}
export const STAGE_BANNER_INJECTION_KEY: InjectionKey<StageBannerContext> =
Symbol("newt-stage-banner")Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { StageBanner } from "@/components/ui/stage-banner"
export function Example() {
return <StageBanner />
}<script setup lang="ts">
import { StageBanner, StageBannerDot, StageBannerAction, StageBannerClose } from "@/components/ui/stage-banner"
</script>
<template>
<StageBanner />
</template>API reference
StageBanner
open?boolean | undefined
Controlled visibility; when omitted the banner manages its own state.
boolean | undefinedonOpenChange?(open: boolean) => void | undefined
Fired when the banner is dismissed via StageBannerClose.
(open: boolean) => void | undefinedThe registry exports StageBanner, StageBannerDot, StageBannerAction, StageBannerClose. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.
Tokens
Stage Banner reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-stage-banner.