Skip to content

@vibe-labs/design-vue-core

Vue 3 core primitives for the Vibe Design System. Foundational building blocks — panels, links, labels, dividers, overflow layout, and accessibility helpers.

Installation

ts
import { VibePanel, VibeLink, VibeLabel, VibeDivider, VibeHidden, VibeOverflowRow } from "@vibe-labs/design-vue-core";

Requires the CSS layer from @vibe-labs/design-components-core.

Components

VibePanel

Content container with variant styles and padding presets. Renders as a <div> by default.

vue
<!-- Basic -->
<VibePanel>Content here</VibePanel>

<!-- Variants -->
<VibePanel variant="default">Solid background</VibePanel>
<VibePanel variant="outlined">With border</VibePanel>
<VibePanel variant="raised">With shadow</VibePanel>
<VibePanel variant="ghost">Transparent background</VibePanel>

<!-- Padding sizes -->
<VibePanel size="sm">Tight padding</VibePanel>
<VibePanel size="md">Default padding</VibePanel>
<VibePanel size="lg">Spacious padding</VibePanel>
<VibePanel size="xl">Extra spacious</VibePanel>

<!-- Flush (no padding) -->
<VibePanel flush>No padding — useful when nesting other components</VibePanel>

<!-- As a section element -->
<VibePanel section aria-label="User profile">
  Renders as <section>
</VibePanel>

<!-- Custom element -->
<VibePanel as="article">Renders as <article></VibePanel>

<!-- Composing: outlined panel with flush inner content -->
<VibePanel variant="outlined" flush>
  <VibePanel size="md">Header area</VibePanel>
  <VibeDivider />
  <VibePanel size="md">Body area</VibePanel>
</VibePanel>

Props

PropTypeDefaultDescription
variantPanelVariant"default"default · outlined · raised · ghost
sizePanelSize"md"sm · md · lg · xl (padding preset)
flushbooleanfalseRemove all padding
asstring | Component"div"Rendered element/component
sectionbooleanfalseRender as <section>

Polymorphic link that resolves to <a>, <router-link>, or a custom element. Auto-detects external URLs.

vue
<!-- Basic anchor -->
<VibeLink href="/about">About</VibeLink>

<!-- Router link -->
<VibeLink :to="{ name: 'Dashboard' }">Dashboard</VibeLink>
<VibeLink to="/settings">Settings</VibeLink>

<!-- External (auto-detected from https://) -->
<VibeLink href="https://example.com">Example</VibeLink>
<!-- Renders: <a href="..." target="_blank" rel="noopener noreferrer"> -->

<!-- Force external -->
<VibeLink href="/api/download" external>Download</VibeLink>

<!-- Variants -->
<VibeLink href="/about" variant="accent">Accent (default)</VibeLink>
<VibeLink href="/about" variant="muted">Muted</VibeLink>
<VibeLink href="/about" variant="inherit">Inherits parent color</VibeLink>

<!-- Active state (current page) -->
<VibeLink href="/dashboard" active>Dashboard</VibeLink>

<!-- Disabled -->
<VibeLink href="/locked" disabled>Locked</VibeLink>

<!-- Custom element -->
<VibeLink :as="NuxtLink" to="/page">Nuxt Link</VibeLink>

Props

PropTypeDefaultDescription
hrefstringURL for native anchor
tostring | objectVue Router route (renders <router-link>)
asstring | ComponentautoOverride element/component
variantLinkVariant"accent"accent · muted · inherit
externalbooleanautoOpen in new tab with rel="noopener noreferrer"
activebooleanfalseActive/current page state
disabledbooleanfalseDisabled state

Resolution Order

  1. If as is provided → use that
  2. If disabled → renders <span> with aria-disabled
  3. If to is provided → resolves <RouterLink> (falls back to <a>)
  4. Otherwise → <a>

External links (https://, //) automatically get target="_blank" and rel="noopener noreferrer".

VibeLabel

Styled label with required indicator support.

vue
<!-- Basic -->
<VibeLabel for="email">Email address</VibeLabel>

<!-- With required asterisk -->
<VibeLabel for="name" required>Full name</VibeLabel>

<!-- Sizes -->
<VibeLabel for="input" size="sm">Small label</VibeLabel>
<VibeLabel for="input" size="md">Medium label</VibeLabel>
<VibeLabel for="input" size="lg">Large label</VibeLabel>

<!-- Disabled -->
<VibeLabel for="locked" disabled>Locked field</VibeLabel>

<!-- Wrapping an input (no for needed) -->
<VibeLabel required>
  Email
  <VibeInput v-model="email" />
</VibeLabel>

Props

PropTypeDefaultDescription
forstringAssociated input ID
sizeLabelSize"md"sm · md · lg
requiredbooleanfalseShow required asterisk
disabledbooleanfalseDisabled styling

VibeDivider

Horizontal or vertical separator with optional label.

vue
<!-- Horizontal (default) -->
<VibeDivider />

<!-- Vertical (in a flex row) -->
<div style="display: flex; align-items: center">
  <span>Left</span>
  <VibeDivider orientation="vertical" />
  <span>Right</span>
</div>

<!-- With label -->
<VibeDivider>or</VibeDivider>
<VibeDivider>Section title</VibeDivider>

<!-- With aria-label for semantics -->
<VibeDivider aria-label="End of search results" />

Props

PropTypeDefaultDescription
orientationDividerOrientation"horizontal"horizontal · vertical
ariaLabelstringAccessible label

Slots

SlotDescription
defaultLabel text (creates a labelled divider with lines on either side)

VibeOverflowRow

Generic overflow-aware horizontal row. Measures child widths via an offscreen clone and shows only as many items as fit, with a "+N" overflow indicator for the rest. Content-agnostic — works with badges, avatars, tags, or any inline element.

vue
<!-- Basic: badge overflow -->
<VibeOverflowRow :items="genres">
  <template #item="{ item }">
    <VibeBadge :label="item.name" auto-color />
  </template>
</VibeOverflowRow>

<!-- Custom overflow indicator -->
<VibeOverflowRow :items="tags" :gap="4">
  <template #item="{ item }">
    <VibeBadge :label="item.label" variant="outline" size="sm" />
  </template>
  <template #overflow="{ hiddenCount }">
    <VibeBadge :label="`+${hiddenCount} more`" variant="accent-subtle" size="sm" />
  </template>
</VibeOverflowRow>

<!-- Overlapping avatars -->
<VibeOverflowRow :items="users" :gap="-8">
  <template #item="{ item }">
    <VibeAvatar :src="item.avatar" :name="item.name" size="sm" />
  </template>
  <template #overflow="{ hiddenCount }">
    <VibeAvatar :label="`+${hiddenCount}`" size="sm" />
  </template>
</VibeOverflowRow>

Props

PropTypeDefaultDescription
itemsT extends { id: string | number }[]Items to render. Each must have a unique id.
gapnumber8Gap between items in px

Slots

SlotPropsDescription
item{ item: T, index: number }Render each visible item. Must be a single root element.
overflow{ hiddenCount: number }Overflow indicator (default: +N text span)

Notes

  • Uses ResizeObserver to re-measure on container or content resize.
  • The #item slot must render a single root element per item for accurate width measurement.
  • Negative gap values work for overlapping layouts (e.g. stacked avatars).

VibeHidden

Screen-reader-only content wrapper. Visually hidden but accessible to assistive technology.

vue
<!-- Basic (visually hidden text) -->
<VibeHidden>This text is only visible to screen readers</VibeHidden>

<!-- Skip link (visible on focus) -->
<VibeHidden focusable as="a" href="#main-content">
  Skip to main content
</VibeHidden>

<!-- Accessible icon button label -->
<button>
  <SearchIcon />
  <VibeHidden>Search</VibeHidden>
</button>

<!-- Custom element -->
<VibeHidden as="div">Hidden block content</VibeHidden>

Props

PropTypeDefaultDescription
focusablebooleanfalseBecome visible on focus (for skip links)
asstring"span"Rendered element

Dependencies

PackagePurpose
@vibe-labs/design-components-coreCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.