Feedback

Promo Banner

A dismissible promo bar with no JS at all: the close control is a label for a hidden checkbox, and :has(:checked) removes the whole bar.

  • banner
  • promo
  • dismiss
  • no-js

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/banner-008?lang=en

Годовая подписка на 30% дешевле до конца недели.

AUTUMN30Get the discount
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "banner-008" (Promo Banner) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/banner-008.json

Registry item: https://vibeui.ru/r/banner-008.json
Installs to: components/vibeui/banner-008.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A dismissible promo bar with no JS at all: the close control is a label for a hidden checkbox, and :has(:checked) removes the whole bar.

A dismissible promo bar: closing works without JS through a checkbox and :has(). Zero dependencies, one file, no client state.

## 3. How to use it
import { Banner008 } from "@/components/vibeui/banner-008"

<Banner008
  message="Annual plans are 30% off."
  code="AUTUMN30"
  actionLabel="Get the discount"
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-banner-008-* palette — do not swap it for your theme tokens
- hiding the checkbox with clip-path rather than display:none — otherwise it leaves the focus order
- the label-to-checkbox link by id: without it the close control stops working
- the :has(:checked) rule on the root — that is the JS-free dismissal mechanism
- the monospaced code on a dashed border: people retype it by eye rather than copy it
- a unique id when two bars share a page — otherwise they close together

## 6. You may change
- the copy through message and the coupon through code
- the button caption through actionLabel and the link target
- the checkbox identifier through id
- the gradient through --vibeui-banner-008-from and --vibeui-banner-008-to

## 7. Rules
- Dismissal is not remembered between visits: to keep the bar away you have to store that yourself.
- The :has() selector works in current browsers; in old ones the bar simply will not close.
- The close label is aria-hidden — the checkbox carries the accessible name, otherwise the action is announced twice.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/banner-008.json
https://vibeui.ru/r/banner-008.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-banner-008-*. Серверный: состояние закрытия живёт в DOM — визуально скрытый чекбокс плюс правило [data-vibeui-block]:has([data-part="switch"]:checked){display:none}. Крестик оформлен тегом <label>, поэтому нажатие переключает чекбокс без обработчика. Чекбокс спрятан clip-path, а не display:none, иначе он потерял бы фокус. Промокод набран моноширинным шрифтом на пунктирной рамке: его переписывают глазами.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Banner008Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  message?: string
  /** Промокод: копировать его глазами проще, чем искать в письме. */
  code?: string
  actionLabel?: string
  /** Уникальный идентификатор чекбокса закрытия: нужен при двух полосах на странице. */
  id?: string
}

// Идея компонента: закрываемая полоса акции без единой строки JS. Крестик —
// это <label> к спрятанному чекбоксу, а :has(:checked) убирает полосу целиком.
// Состояние живёт в DOM, поэтому компонент остаётся серверным.
const STYLES = `
:where([data-vibeui-block="banner-008"]){
--vibeui-banner-008-from:oklch(0.42 0.15 315);
--vibeui-banner-008-to:oklch(0.46 0.16 265);
--vibeui-banner-008-fg:oklch(0.98 0.01 315);
--vibeui-banner-008-muted:oklch(0.86 0.04 315);
--vibeui-banner-008-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-banner-008-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
container-type:inline-size;
}
[data-vibeui-block="banner-008"]{
width:100%;box-sizing:border-box;
font-family:var(--vibeui-banner-008-font);color:var(--vibeui-banner-008-fg);
}
/* Закрытие без JS: чекбокс спрятан, :has(:checked) убирает всю полосу. */
[data-vibeui-block="banner-008"] [data-part="switch"]{
position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;border:0;
}
[data-vibeui-block="banner-008"]:has([data-part="switch"]:checked){display:none}
[data-vibeui-block="banner-008"] [data-part="shell"]{
display:flex;align-items:center;gap:0.875rem;flex-wrap:wrap;
box-sizing:border-box;padding:0.75rem 0.875rem 0.75rem 1.125rem;
border-radius:0.875rem;
background:linear-gradient(100deg,var(--vibeui-banner-008-from),var(--vibeui-banner-008-to));
}
[data-vibeui-block="banner-008"] [data-part="message"]{
margin:0;flex:1 1 13rem;min-width:0;font-size:0.8125rem;line-height:1.45;
}
[data-vibeui-block="banner-008"] [data-part="code"]{
flex:none;padding:0.1875rem 0.5rem;border-radius:0.4375rem;
border:1px dashed oklch(1 0 0 / 45%);background:oklch(1 0 0 / 14%);
font-family:var(--vibeui-banner-008-mono);font-size:0.75rem;font-weight:700;letter-spacing:0.06em;
}
[data-vibeui-block="banner-008"] [data-part="action"]{
flex:none;display:inline-flex;align-items:center;height:1.9375rem;padding:0 0.8125rem;
border-radius:0.5rem;background:oklch(1 0 0);color:var(--vibeui-banner-008-from);
font-size:0.8125rem;font-weight:700;text-decoration:none;
transition:transform .16s ease;
}
[data-vibeui-block="banner-008"] [data-part="action"]:hover{transform:translateY(-1px)}
[data-vibeui-block="banner-008"] [data-part="close"]{
flex:none;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;
width:1.75rem;height:1.75rem;border-radius:0.5rem;
color:var(--vibeui-banner-008-muted);font-size:1rem;line-height:1;
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="banner-008"] [data-part="close"]:hover{background:oklch(1 0 0 / 16%);color:var(--vibeui-banner-008-fg)}
[data-vibeui-block="banner-008"] [data-part="switch"]:focus-visible + [data-part="shell"] [data-part="close"]{outline:2px solid oklch(1 0 0);outline-offset:2px}
[data-vibeui-block="banner-008"] [data-part="action"]:focus-visible{outline:2px solid oklch(1 0 0);outline-offset:2px}
@container (max-width: 26rem){
[data-vibeui-block="banner-008"] [data-part="action"]{flex:1 1 100%;justify-content:center}
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="banner-008"] *{animation:none!important;transition:none!important}}
`

/**
 * Закрываемая полоса акции: крестик работает без JS, через чекбокс и :has().
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Banner008({
  message = "Годовая подписка на 30% дешевле до конца недели.",
  code = "AUTUMN30",
  actionLabel = "Забрать скидку",
  id = "vibeui-banner-008",
  className,
  style,
  ...props
}: Banner008Props) {
  return (
    <>
      <style href="vibeui-banner-008" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="banner-008"
        className={className}
        style={style as CSSProperties}
      >
        <input
          data-part="switch"
          id={id}
          type="checkbox"
          aria-label="Скрыть полосу акции"
        />
        <div data-part="shell">
          <p data-part="message">{message}</p>
          {code ? <span data-part="code">{code}</span> : null}
          <a data-part="action" href="#pricing">
            {actionLabel}
          </a>
          <label data-part="close" htmlFor={id} aria-hidden="true">
            ×
          </label>
        </div>
      </div>
    </>
  )
}