Menu
Anchored Dropdown
A dropdown with no JS: the HTML popover attribute handles opening, and CSS anchor positioning keeps it next to its button.
- dropdown
- menu
- popover
- no-js
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/dropdown-001?lang=en
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 "dropdown-001" (Anchored Dropdown) 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/dropdown-001.json
Registry item: https://vibeui.ru/r/dropdown-001.json
Installs to: components/vibeui/dropdown-001.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 dropdown with no JS: the HTML popover attribute handles opening, and CSS anchor positioning keeps it next to its button.
A menu on a button: opening, Esc and click-outside come from the HTML popover attribute, and the placement beside the button — including flipping when there is no room — from CSS anchor positioning. The items are plain links and the destructive one is set apart by a rule. Zero dependencies, one file, its own palette, no client JS.
## 3. How to use it
import { Dropdown001 } from "@/components/vibeui/dropdown-001"
<Dropdown001
id="project-menu"
trigger="Project"
items={[
{ label: "Settings", href: "/settings" },
{ label: "Delete", href: "/delete", separated: true, danger: true },
]}
/>
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-dropdown-001-* palette — do not swap it for your theme tokens (bg-popover, border-border and the like)
- the popover mechanism: popovertarget on the button and popover="auto" on the menu are the whole behaviour
- the anchor-name / position-anchor pair together with position-area: without them a top-layer menu lands in the middle of the screen
- the @supports block around anchor positioning: where anchors are missing the menu must still work, even if centred
- styling the menu by attribute and passing the palette through its own style: in the top layer it is no longer a descendant of the root
- position-try-fallbacks: near the bottom edge the menu has to flip rather than run off screen
- the rule above the destructive item and its separate colour — Delete must not sit flush against ordinary items
- the <style> block inside the component — it holds the palette, the positioning and the animation
## 6. You may change
- the items array: labels, hrefs, the separated and danger flags
- trigger — the button label
- id — one per menu on the page
- open — expand the menu in the page flow: showcase, screenshots, debugging
- the accent through the accent prop — it colours the focus ring
## 7. Rules
- Keyboard navigation inside the menu is limited to Tab: arrow keys between items mean role="menu" and a handler, which means JS.
- Two menus sharing an id conflict: the button opens whichever it finds first.
- The items are links. If an item runs an action, swap <a> for <button type="button"> in your copy.
- 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/dropdown-001.jsonhttps://vibeui.ru/r/dropdown-001.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-dropdown-001-*. Открытие — атрибуты popover и popovertarget, положение — anchor-name у кнопки и position-anchor с position-area у меню, под @supports. Меню живёт в верхнем слое, поэтому стилизуется по атрибуту [data-vibeui-dropdown-001-menu] и получает палитру собственным style. Проп open переводит меню в поток страницы для витрины и скриншотов: popover в этом режиме не используется.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
import type { CSSProperties } from "react"
export type Dropdown001Item = {
label: string
href?: string
/** Отделяет пункт от предыдущего линией: «Выйти», «Удалить». */
separated?: boolean
danger?: boolean
}
export type Dropdown001Props = {
id?: string
/**
* Показать меню развёрнутым в потоке страницы: витрина, скриншот, отладка.
* В этом режиме popover не используется, поэтому Esc и клик мимо не работают.
*/
open?: boolean
trigger?: string
items?: Dropdown001Item[]
accent?: string
className?: string
style?: CSSProperties
}
// Идея компонента: меню без JS. Открытие держит HTML popover, а положение
// рядом с кнопкой — CSS anchor positioning: меню привязано к триггеру якорем,
// а не пересчитывается скриптом на каждый скролл. Там, где якоря ещё нет,
// меню открывается по центру экрана — некрасиво, но работает.
const STYLES = `
:where([data-vibeui-block="dropdown-001"]){
--vibeui-dropdown-001-fg:oklch(0.24 0.016 265);
--vibeui-dropdown-001-muted:oklch(0.52 0.014 265);
--vibeui-dropdown-001-bg:oklch(1 0 0);
--vibeui-dropdown-001-border:oklch(0.9 0.006 265);
--vibeui-dropdown-001-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-dropdown-001-accent:oklch(0.55 0.2 262);
--vibeui-dropdown-001-danger:oklch(0.56 0.19 25);
--vibeui-dropdown-001-radius:0.625rem;
--vibeui-dropdown-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="dropdown-001"]{display:inline-flex;font-family:var(--vibeui-dropdown-001-font)}
[data-vibeui-block="dropdown-001"] [data-part="trigger"]{
appearance:none;cursor:pointer;
display:inline-flex;align-items:center;gap:0.5rem;
height:2.25rem;padding:0 0.875rem;
border:1px solid var(--vibeui-dropdown-001-border);border-radius:0.5rem;
background:var(--vibeui-dropdown-001-bg);color:var(--vibeui-dropdown-001-fg);
font:inherit;font-size:0.875rem;font-weight:500;
anchor-name:--vibeui-dropdown-001-anchor;
transition:border-color .16s ease,background-color .16s ease;
}
[data-vibeui-block="dropdown-001"] [data-part="trigger"]:hover{background:var(--vibeui-dropdown-001-hover)}
[data-vibeui-block="dropdown-001"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-dropdown-001-accent);outline-offset:2px}
[data-vibeui-block="dropdown-001"] [data-part="chevron"]{
width:0.375rem;height:0.375rem;
border-right:1.5px solid var(--vibeui-dropdown-001-muted);
border-bottom:1.5px solid var(--vibeui-dropdown-001-muted);
transform:rotate(45deg) translate(-0.0625rem,-0.0625rem);
}
[data-vibeui-dropdown-001-menu]{
position:fixed;margin:0;padding:0.3125rem;
min-width:12rem;box-sizing:border-box;
border:1px solid var(--vibeui-dropdown-001-border,oklch(0.9 0.006 265));
border-radius:var(--vibeui-dropdown-001-radius,0.625rem);
background:var(--vibeui-dropdown-001-bg,oklch(1 0 0));
color:var(--vibeui-dropdown-001-fg,oklch(0.24 0.016 265));
font-family:var(--vibeui-dropdown-001-font,ui-sans-serif,system-ui,sans-serif);
box-shadow:0 16px 36px -18px oklch(0.2 0.03 265 / 45%);
opacity:0;transform:translateY(-0.25rem);
transition:opacity .14s ease,transform .14s ease,display .14s allow-discrete,overlay .14s allow-discrete;
}
[data-vibeui-dropdown-001-menu]:popover-open{opacity:1;transform:none}
@starting-style{[data-vibeui-dropdown-001-menu]:popover-open{opacity:0;transform:translateY(-0.25rem)}}
/* Привязка к кнопке. Без поддержки якорей меню останется по центру экрана. */
@supports (anchor-name: --a){
[data-vibeui-dropdown-001-menu]{
position-anchor:--vibeui-dropdown-001-anchor;
position-area:bottom span-right;
margin-top:0.375rem;
position-try-fallbacks:flip-block,flip-inline;
}
}
[data-vibeui-dropdown-001-menu] [data-part="item"]{
display:flex;align-items:center;width:100%;box-sizing:border-box;
padding:0.4375rem 0.5625rem;border-radius:0.4375rem;
color:inherit;text-decoration:none;font-size:0.875rem;line-height:1.3;
transition:background-color .14s ease;
}
[data-vibeui-dropdown-001-menu] [data-part="item"]:hover{background:var(--vibeui-dropdown-001-hover,oklch(0.55 0.02 265 / 8%))}
[data-vibeui-dropdown-001-menu] [data-part="item"]:focus-visible{outline:2px solid var(--vibeui-dropdown-001-accent,oklch(0.55 0.2 262));outline-offset:-2px}
[data-vibeui-dropdown-001-menu] [data-part="item"][data-danger="true"]{color:var(--vibeui-dropdown-001-danger,oklch(0.56 0.19 25))}
[data-vibeui-dropdown-001-menu] [data-part="item"][data-separated="true"]{
margin-top:0.3125rem;padding-top:0.5625rem;
border-top:1px solid var(--vibeui-dropdown-001-border,oklch(0.9 0.006 265));
border-radius:0 0 0.4375rem 0.4375rem;
}
/* Развёрнутый режим: меню стоит в потоке под кнопкой, а не в верхнем слое. */
[data-vibeui-block="dropdown-001"]:has([data-open="true"]){flex-direction:column;align-items:flex-start}
[data-vibeui-dropdown-001-menu][data-open="true"]{
position:static;opacity:1;transform:none;margin-top:0.375rem;
}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="dropdown-001"] *{animation:none!important;transition:none!important}
[data-vibeui-dropdown-001-menu]{transition:none!important;opacity:1;transform:none}
}
`
const DEFAULT_ITEMS: Dropdown001Item[] = [
{ label: "Настройки проекта", href: "#" },
{ label: "Участники", href: "#" },
{ label: "История публикаций", href: "#" },
{ label: "Удалить проект", href: "#", separated: true, danger: true },
]
/**
* Выпадающее меню на HTML popover и якорном позиционировании: без JS.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Dropdown001({
id = "vibeui-dropdown-001",
open = false,
trigger = "Проект",
items = DEFAULT_ITEMS,
accent,
className,
style,
}: Dropdown001Props) {
const palette = {
...(accent ? { "--vibeui-dropdown-001-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-dropdown-001" precedence="medium">
{STYLES}
</style>
<div
data-vibeui-block="dropdown-001"
className={className}
style={palette}
>
<button data-part="trigger" type="button" popoverTarget={id}>
{trigger}
<span data-part="chevron" aria-hidden="true" />
</button>
<div
id={id}
popover={open ? undefined : "auto"}
data-vibeui-dropdown-001-menu=""
data-open={open || undefined}
style={palette}
>
{items.map((item) => (
<a
key={item.label}
data-part="item"
data-danger={item.danger || undefined}
data-separated={item.separated || undefined}
href={item.href}
>
{item.label}
</a>
))}
</div>
</div>
</>
)
}