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.

Usage

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</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 -->
<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.

Usage

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 -->
<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.

Usage

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="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.

Usage

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>

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. Content-agnostic — works with badges, avatars, tags, or any inline element.

Usage

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.

Usage

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>

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.


Usage Guide

Setup

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

VibePanel

Composing layout panels

vue
<script setup lang="ts">
import { VibePanel } from "@vibe-labs/design-vue-core";
import { VibeDivider } from "@vibe-labs/design-vue-core";
</script>

<template>
  <!-- Outlined container with internal sections -->
  <VibePanel variant="outlined" flush>
    <VibePanel size="md">
      <h3>Section Header</h3>
    </VibePanel>
    <VibeDivider />
    <VibePanel size="md">
      <p>Main content area</p>
    </VibePanel>
  </VibePanel>
</template>
vue
<script setup lang="ts">
import { VibeLink } from "@vibe-labs/design-vue-core";
import { RouterLink } from "vue-router";

defineProps<{ currentPath: string }>();
</script>

<template>
  <!-- Internal router link -->
  <VibeLink :to="{ name: 'Dashboard' }">Dashboard</VibeLink>

  <!-- External — auto-detects https:// and adds target="_blank" -->
  <VibeLink href="https://docs.example.com" variant="muted">Documentation</VibeLink>

  <!-- Active state for current route -->
  <VibeLink to="/profile" :active="currentPath === '/profile'">Profile</VibeLink>
</template>

VibeOverflowRow

Tag overflow with badges

vue
<script setup lang="ts">
import { VibeOverflowRow } from "@vibe-labs/design-vue-core";
import { VibeBadge } from "@vibe-labs/design-vue-badges";
import { ref } from "vue";

const tags = ref([
  { id: 1, label: "Electronic" },
  { id: 2, label: "Deep House" },
  { id: 3, label: "Minimal" },
  { id: 4, label: "Techno" },
  { id: 5, label: "Ambient" },
]);
</script>

<template>
  <VibeOverflowRow :items="tags" :gap="6">
    <template #item="{ item }">
      <VibeBadge :label="item.label" auto-color size="sm" />
    </template>
    <template #overflow="{ hiddenCount }">
      <VibeBadge :label="`+${hiddenCount} more`" variant="accent-subtle" size="sm" />
    </template>
  </VibeOverflowRow>
</template>

VibeDivider

Section separator and "or" divider

vue
<template>
  <VibePanel size="md">
    <p>Login with email</p>
    <VibeDivider>or</VibeDivider>
    <p>Continue with Google</p>
  </VibePanel>
</template>

VibeHidden

vue
<template>
  <!-- Screen-reader label on icon-only button -->
  <button>
    <SearchIcon aria-hidden="true" />
    <VibeHidden>Search</VibeHidden>
  </button>

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

Common Patterns

Form field with label

vue
<script setup lang="ts">
import { VibeLabel } from "@vibe-labs/design-vue-core";
import { VibeInput } from "@vibe-labs/design-vue-inputs";
import { ref } from "vue";

const email = ref("");
</script>

<template>
  <div>
    <VibeLabel for="email-field" required>Email address</VibeLabel>
    <VibeInput id="email-field" v-model="email" type="email" />
  </div>
</template>

Vibe