BVMoreButton
A button that opens a dropdown menu of secondary outbound links: GitHub repo, report a bug, request a feature, and the like. It sits next to a primary call to action without giving those secondary links equal visual weight. Modeled on the pattern used by Obsidian's community-plugin pages.
By default the button shows visible text reading "More..." (auto-width, same padding/sizing as BVPlatformButton). Pass your own text to change the label, or an empty string (text="") to switch to the icon-only mode instead — three dots (⋯) at a small fixed size, no visible text; see Props below.
BVMoreButton renders its trigger through BVIconButton internally rather than owning its own button markup/CSS — its own logic (the menu panel, keyboard handling, placement algorithm) is unchanged either way, but on the /vitepress import path, that means text mode (any non-empty text) now renders through VitePress's real VPButton and gets its theme styling automatically, same as BVPlatformButton. Icon-only mode (text="") still renders through a hand-rolled button on both paths — VPButton has no concept of a fixed-size icon-only shape.
Two independent implementations, same as BVPlatformButton. Import it from either path; the /vitepress path gets real VPButton styling for free, so prefer it inside a VitePress site:
import { BVMoreButton } from 'bojuvue'
// or, inside a VitePress site, for real VPButton styling in text mode:
import { BVMoreButton } from 'bojuvue/vitepress'Dropdown panel looks unstyled? You're missing the stylesheet import
BVMoreButton's trigger renders through the real VPButton (or a hand-rolled equivalent), so it can look basically fine even without BojuVue's own CSS loaded — but the dropdown panel itself (background, border, shadow, item spacing, icon sizing) is all BojuVue-authored, not VitePress's. If it opens as plain unstyled links instead of a floating card, you're missing:
import 'bojuvue/style.css'See Import the stylesheet — this is the one setup step it's easiest to skip, since everything else about a component can look fine without it right up until you open this menu.
Demo
The first button above is the default — visible text reading "More...". The second is the same component with text="", switching to the icon-only three-dot mode instead.
Try it with a keyboard: focus one of the buttons above and press ArrowDown (jumps to the first item) or ArrowUp (jumps to the last item), then ArrowDown/ArrowUp to move between items (wrapping at both ends), Home/End to jump to the first/last item, and Escape to close and return focus to the button.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | BVMoreButtonItem[] | (required) | The menu's contents, in order. See below for the item shape. |
text | string | 'More...' | Visible button text. Pass an empty string (text="") to switch to icon-only mode instead (fixed-size button, no visible text). With any non-empty text, the button is an auto-width pill layout, showing icon (if also given) next to it. |
icon | string | (none) | Raw SVG markup rendered via v-html. In icon-only mode (text=""), replaces the built-in three-dot icon. With text, only rendered if you explicitly set this — otherwise the button shows text alone, no icon. Rendered unescaped — caller-supplied only, never fed anything dynamic/untrusted; see the warning below. |
label | string | 'More options' | Sets the button's aria-label. Only applied in icon-only mode (text="") — with visible text, the accessible name comes from that text content instead, so this prop is ignored rather than layered on top. |
size | 'medium' | 'big' | 'medium' | Sets the button's dimensions: 'medium' is a 38px fixed size (icon-only) or a 38px-tall pill (with text); 'big' is 46px either way. |
theme | 'brand' | 'alt' | 'sponsor' | 'brand' | Sets the button's color scheme via a modifier class: 'brand' is a solid accent color, 'alt' is neutral/muted, 'sponsor' is a pink accent. See Styling below for where these colors come from. |
BVMoreButtonItem:
| Field | Type | Default | Description |
|---|---|---|---|
label | string | (required) | The menu item's visible text. |
href | string | (required) | The link target. Rendered as a real <a role="menuitem">, so Enter-to-activate and middle-click/ctrl-click-to-open-in-new-tab work natively with no extra code. |
icon | string | (none) | Raw SVG markup rendered via v-html next to the label. Caller-authored only — the items array is something you write yourself, never unescaped user input. |
target | string | (none) | Left unset by default so a smart default applies: target="_blank" when href looks external. Set it explicitly only to override that. |
rel | string | (none) | Left unset by default so a smart default applies: rel="noreferrer" when href looks external. Set it explicitly only to override that. |
icon is rendered unescaped
Both the button's icon and each item's icon go through v-html with no sanitization. items is something you write yourself, not fetched or user-supplied, so this is safe as long as it stays that way. Never wire items up to anything dynamic without escaping it first.
It renders as a real <button> element (not an <a>) since it has no href of its own — it only ever toggles the menu.
Styling
On the bare bojuvue import, the button (via BVIconButton/BVButton) reads the same public --vp-button-* CSS custom properties VitePress itself exposes for theming, each with a fallback value so it still looks like a clickable button outside a VitePress site (where those variables are undefined) — the same tokens BVPlatformButton's generic build also reads. On the /vitepress import, text mode renders through VitePress's real VPButton instead (no fallback values needed — this build only ever runs inside a real VitePress site), while icon-only mode still reads those same custom properties directly. The dropdown panel reads VitePress's public --vp-c-* design tokens for its background, border, and text color — with fallback values on the bare import, without them on /vitepress.
Keyboard behavior
Following the WAI-ARIA menu button pattern:
- Enter/Space on the button toggles the menu open/closed — native
<button>behavior, no custom handling needed. ArrowDownon the button while the menu is closed opens it and moves focus to the first item.ArrowUpin the same state opens it and moves focus to the last item — lets a keyboard user jump straight to the last option without arrowing through everything.- While the menu is open,
ArrowDown/ArrowUpmove real DOM focus between items, wrapping at both ends.Home/Endjump to the first/last item. Escapecloses the menu from anywhere inside it and returns focus to the button.- Clicking outside the component closes the menu, with no focus change.
- Enter on a focused item navigates natively — it's a real anchor with real DOM focus, so nothing is intercepted.
Deliberately not handled: no auto-close on window blur/tab switch, and no special handling when a menu item is activated. Ordinary browser navigation already does the right thing for an outbound link, so there's nothing extra to add there.
Placement
The panel's preferred alignment is leading-edge — its left edge flush with the button's left edge, opening below and to the right. It's still clamped to stay on screen at any viewport width, computed at open time and recomputed on window resize and scroll while open (the panel is position: fixed in viewport coordinates, so without recomputing on scroll it would stay put while the button scrolled away underneath it) via a three-step algorithm:
- Try leading-edge — the panel's left edge flush with the button's left edge. Use it if it doesn't clip past the right edge of the viewport.
- Otherwise flip to trailing-edge — the panel's right edge flush with the button's right edge — which kicks in when the button sits close to the right edge of the viewport. Use it if it doesn't clip past the left edge.
- Otherwise (the viewport is narrower than the panel) clamp the panel inside the viewport as a last resort.
Spacing
Like BVPlatformButton, BVMoreButton claims no margin on itself — spacing between it and a neighboring primary CTA is a caller/layout concern. Use a flex container with gap, the way the demo above is wrapped.
Explicitly out of scope
- No VitePress-aware convenience.
itemsis always a plain prop you pass in explicitly — this component never readsuseData().themeConfig.socialLinksor anything else VitePress-specific for you. You always know exactly what's in the menu, because you're the one who put it there. - No nested/submenu items. Every item is a flat, single-level link.
- No manifest/remote-fetched items.
itemsis always something the caller already has in hand — no async state, no loading state to design for.
Usage
BVMoreButton works the same from either import path — this example uses the bare package, so it's just as valid in a non-VitePress Vue 3 app:
<script setup>
import { BVMoreButton, BVPlatformButton } from 'bojuvue'
</script>
<template>
<div style="display: flex; gap: 12px; align-items: center;">
<BVPlatformButton fallback-href="https://github.com/your-org/your-repo/releases" />
<BVMoreButton
:items="[
{
label: 'GitHub repo',
href: 'https://github.com/your-org/your-repo',
icon: "<svg viewBox='0 0 24 24' width='16' height='16'>...</svg>",
},
{ label: 'Report a bug', href: 'https://github.com/your-org/your-repo/issues/new' },
]"
/>
</div>
</template>Icon-only mode, with a custom icon, label, theme, and size:
<BVMoreButton
text=""
label="Plugin options"
theme="alt"
size="big"
icon="<svg viewBox='0 0 24 24' width='16' height='16'><path d='M12 2 2 22h20z'/></svg>"
:items="items"
/>Overriding the default "More..." text:
<BVMoreButton text="More options" :items="items" />