익스클루시브
Announcement Bar <mv-announcement-bar>
출시, 세일, 점검 공지, 기능 예고를 위한 사이트 상단의 얇은 바입니다. Dismiss 버튼의 닫힘 상태는 id-key별로 localStorage에 기억되고(새 캠페인 id는 다시 표시됨), 선택적인 until 날짜로 스스로 내려가게 할 수 있으며, info, accent, warning 톤과 슬롯으로 넣는 카운트다운을 위한 공간을 제공합니다.
작동 방식
출시, 세일, 점검 공지, 기능 예고를 위한 사이트 상단의 얇은 바입니다. Dismiss 버튼의 닫힘 상태는 id-key별로 localStorage에 기억되고(새 캠페인 id는 다시 표시됨), 선택적인 until 날짜로 스스로 내려가게 할 수 있으며, info, accent, warning 톤과 슬롯으로 넣는 카운트다운을 위한 공간을 제공합니다. 다른 구현이 놓치는 세부 사항: 레이아웃이 절대 밀리지 않습니다(JavaScript 실행 전에 태그 스타일과 버튼 공간이 확보되고, 한 줄짜리 head 스크립트가 이전 방문에서 닫은 바가 아예 그려지지 않게 합니다). 라이브 리전이 아닌 레이블이 지정된 리전이므로 페이지를 불러올 때마다 스크린 리더가 방해받지 않습니다. 닫으면 포커스를 <body>에 떨어뜨리지 않고 다음 포커스 가능한 요소(없으면 <main>)로 넘기며, 연속된 바는 하나의 띠로 쌓입니다.
| 카테고리 | 피드백 |
|---|---|
| 유형 | Web Component (<mv-announcement-bar>) |
| 상태 | 안정 |
| Keywords | exclusive, light, announcement, banner, top-bar, promo, launch, maintenance, dismissible, cls |
When to use
- A launch, sale or new feature needs a thin site-wide notice above the header that visitors can close for good
- A maintenance window or policy change must stay visible until a set date and then disappear without a deploy
- A promo should count down to its end inside the top bar and stop showing once the campaign is over
- Several notices (a launch and a maintenance warning) may need to stack at the top of the page
Avoid when
- The message is about a section or a form and belongs next to that content, with a title and actions → use Alert instead
- The feedback follows a user action and should disappear on its own after a few seconds → use Toast instead
- A dismissed banner should leave a small badge the user can reopen later → use Grin instead
설치
node scripts/add.mjs announcement-bar --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["announcement-bar"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/element.js, core/focus.js, core/motion.js, components/announcement-bar/announcement-bar.js, components/announcement-bar/announcement-bar.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-announcement-bar id-key="launch-2026-10">Marvelous UI launches Oct 7 · <a href="/waitlist">Join the waitlist</a></mv-announcement-bar>
<!-- optional, in <head>, so a bar dismissed earlier never paints: -->
<script>try{var k=JSON.parse(localStorage.getItem("mv-announcement-bar")||"[]"),s=document.createElement("style");s.textContent=k.map(function(i){return 'mv-announcement-bar[id-key="'+CSS.escape(i)+'"]:not([data-state])'}).join()+"{display:none}";k.length&&document.head.append(s)}catch(e){}</script>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="mv-abar-demo">
<style>
#mv-abar-demo { display: grid; gap: 1.25rem; width: min(100%, 52rem); margin-inline: auto; }
#mv-abar-demo .page { overflow: hidden; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); box-shadow: var(--mv-shadow-md); }
#mv-abar-demo .site { display: flex; align-items: center; gap: 1rem; padding: .75rem 1.25rem; border-bottom: 1px solid var(--mv-border); }
#mv-abar-demo .brand { display: flex; align-items: center; gap: .5rem; font-weight: 650; letter-spacing: -.01em; color: var(--mv-fg); text-decoration: none; border-radius: var(--mv-radius-sm); }
#mv-abar-demo .brand span { width: 1.25rem; height: 1.25rem; border-radius: .35rem; background: linear-gradient(135deg, var(--mv-accent), var(--mv-accent-fg)); }
#mv-abar-demo .site nav { display: flex; gap: .15rem; margin-inline-start: auto; }
#mv-abar-demo .site nav a { padding: .35rem .6rem; border-radius: var(--mv-radius-md); font-size: .875rem; color: var(--mv-fg-muted); text-decoration: none; }
#mv-abar-demo .site a:focus-visible { outline: 2px solid var(--mv-ring); outline-offset: 1px; }
#mv-abar-demo main { padding: 1.75rem 1.5rem 2rem; }
#mv-abar-demo main:focus-visible { outline: 2px solid var(--mv-ring); outline-offset: -2px; }
#mv-abar-demo h2 { margin: 0 0 .4rem; font-size: 1.4rem; letter-spacing: -.02em; }
#mv-abar-demo main p { margin: 0; max-width: 34rem; color: var(--mv-fg-muted); font-size: .92rem; line-height: 1.6; }
#mv-abar-demo .variants { display: grid; gap: .75rem; }
#mv-abar-demo .variants mv-announcement-bar { border-radius: var(--mv-radius-lg); }
#mv-abar-demo .foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .75rem; font-size: .8rem; color: var(--mv-fg-subtle); }
@media (max-width: 36rem) { #mv-abar-demo .site nav a:not(:last-child) { display: none; } }
</style>
<div class="page">
<mv-announcement-bar id-key="launch-2026-10" tone="accent">
Marvelous UI launches Oct 7 · <a href="#">Join the waitlist</a>
</mv-announcement-bar>
<mv-announcement-bar id-key="maintenance-2026-10-04" tone="warning" permanent id="mv-abar-demo-maint">
Scheduled maintenance Sunday, Oct 4, 2:00-3:00 AM UTC. Checkout may be briefly unavailable.
</mv-announcement-bar>
<header class="site">
<a class="brand" href="#"><span></span>Tidewell Studio</a>
<nav aria-label="Main">
<a href="#">Pricing</a>
<a href="#">Docs</a>
<a href="#">Sign in</a>
</nav>
</header>
<main>
<h2>Ship your storefront in 12 languages</h2>
<p>Two bars stacked above the header: the launch notice can be dismissed and stays gone on reload; the maintenance notice has no close button and retires itself when its date passes.</p>
</main>
</div>
<div class="variants">
<mv-announcement-bar id-key="summer-sale-2026">
Summer sale: 30% off annual plans · ends in <mv-countdown data-variant="compact" data-offset="187200" to="2026-10-01T18:00:00-04:00" label="Summer sale">Oct 1, 6:00 PM EDT<template slot="ended">a moment</template></mv-countdown> · <a href="#">Upgrade for $84/year</a>
</mv-announcement-bar>
<mv-announcement-bar id-key="dark-dashboards" tone="info">
New: dark mode for dashboards, shared by Aiko Tanaka’s team · <a href="#">See what’s new</a>
</mv-announcement-bar>
</div>
<div class="foot">
<span>Dismissals are remembered per <code>id-key</code>; a new campaign id shows again.</span>
<button class="mv-button" data-variant="ghost" data-size="sm" id="mv-abar-demo-reset">Show dismissed bars again</button>
</div>
</div>
<script type="module">
// Demo only: keep the dates ahead of today so every bar and timer stays live.
const root = document.getElementById("mv-abar-demo");
root.querySelectorAll("mv-countdown[data-offset]").forEach((el) => { el.to = new Date(Date.now() + Number(el.dataset.offset) * 1000).toISOString(); });
document.getElementById("mv-abar-demo-maint").until = new Date(Date.now() + 3 * 86400000).toISOString();
document.getElementById("mv-abar-demo-reset").addEventListener("click", () => {
root.querySelectorAll("mv-announcement-bar").forEach((bar) => bar.reset());
});
</script>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
id-key | string | Campaign id the dismissal is remembered under (localStorage key mv-announcement-bar, a JSON list of the last 50 dismissed ids). Change it for a new campaign and the bar shows again. Without it, a hash of the bar’s own text is used (the head script then cannot hide it before paint). | |
until | ISO 8601 date-time | Moment after which the bar hides itself, on load or live while the page is open (e.g. 2026-10-14T23:59:00+02:00). Invalid or missing: no end. | |
tone | info | accent | warning | (neutral ink) | Color: neutral dark ink, accent, tinted info or tinted warning. Works in light and dark themes. |
permanent | boolean | No Dismiss button and stored dismissals are ignored (a maintenance notice that must stay until until). | |
data-state | open | dismissed | Set by the component on upgrade (for styling); the head script only hides bars that do not have it yet. |
Properties
| Name | 유형 | Description |
|---|---|---|
idKey / until / permanent | string / string / boolean | Mirror the attributes. |
strings | object | Override default texts: label (region name, “Announcement”) and dismiss (button name, “Dismiss announcement”). An aria-label or aria-labelledby you set on the bar is kept. |
dismissKey | string | Key the dismissal is stored under: id-key, else auto-<hash of the text> (read-only). |
dismissed | boolean | True while the bar is hidden because it was dismissed, remembered or expired (read-only). |
MvAnnouncementBar.earlyScript | string (static) | The <head> snippet as a string, to inline from a server template or framework layout. Also exported as EARLY_SCRIPT. |
Methods
| Name | Description |
|---|---|
dismiss() | Hides the bar (height collapse), remembers its key and moves focus on if it was inside; returns false if vetoed or already hidden. |
reset() | Forgets the dismissal for this key and shows the bar again (unless until has passed). |
Events
| Name | Description |
|---|---|
mv-dismiss | detail = { key, reason: "user" | "expired" }. Cancelable when reason is user: preventDefault() keeps the bar open and nothing is stored. Also fired (not cancelable) when until passes while the bar is visible. |
Content structure
| Name | Description |
|---|---|
(content) | The message: text, links, a <mv-countdown data-variant="compact"> or any inline content. Nested custom elements are ignored when hashing the text for the automatic key. |
CSS classes
| Name | Description |
|---|---|
mv-announcement-bar-dismiss | Generated Dismiss button (end side, aligned to the first line). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-announcement-bar-bg | per tone | Background color. |
--mv-announcement-bar-fg | per tone | Text, link and icon color. |
--mv-announcement-bar-border | per tone | Bottom hairline color (transparent for the neutral and accent tones). |
--mv-announcement-bar-height | 2.5rem | Minimum height, reserved before JavaScript runs. |
Accessibility
The bar gets role=region with the accessible name “Announcement” (strings.label, or your own aria-label), so it is listed with landmarks and can be skipped, but it is not a live region: nothing is read out on page load or navigation. Put it first in <body>, before the header, so it comes first in reading and tab order. The Dismiss button is a real <button> named “Dismiss announcement” (strings.dismiss), the × icon is aria-hidden, the glyph stays small while the hit area grows to about 42 px, focus is a visible 2 px outline in the bar’s text color on every tone. When the bar is dismissed (or expires) while focus is inside it, focus moves to the next focusable element after it (the next stacked bar, the logo, the nav), else to <main> (made focusable with tabindex=-1 for that moment), never to <body>; the bar is inert while it collapses. The collapse is a short height and opacity animation, instant under prefers-reduced-motion or data-motion=reduce. No layout shift: the tag is styled and the button space reserved by CSS alone, so the upgrade changes nothing; for returning visitors, the optional <head> snippet (quickstart, or MvAnnouncementBar.earlyScript) hides dismissed id-keys before first paint. Forced colors: system border and button colors. Hidden when printing. Dismissals sync across open tabs. SSR-safe: nothing touches window or document at import, and the content is plain HTML readable without JavaScript. Known limits: when localStorage is blocked (some private modes, strict storage policies), a dismissal lasts only for the current page view; the optional head snippet has to be pasted by hand in the page <head>, the component cannot inject it early enough itself.