Appearance
@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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | PanelVariant | "default" | default · outlined · raised · ghost |
size | PanelSize | "md" | sm · md · lg · xl (padding preset) |
flush | boolean | false | Remove all padding |
as | string | Component | "div" | Rendered element/component |
section | boolean | false | Render as <section> |
VibeLink
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
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | URL for native anchor |
to | string | object | — | Vue Router route (renders <router-link>) |
as | string | Component | auto | Override element/component |
variant | LinkVariant | "accent" | accent · muted · inherit |
external | boolean | auto | Open in new tab with rel="noopener noreferrer" |
active | boolean | false | Active/current page state |
disabled | boolean | false | Disabled state |
Resolution Order
- If
asis provided → use that - If
disabled→ renders<span>witharia-disabled - If
tois provided → resolves<RouterLink>(falls back to<a>) - 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
| Prop | Type | Default | Description |
|---|---|---|---|
for | string | — | Associated input ID |
size | LabelSize | "md" | sm · md · lg |
required | boolean | false | Show required asterisk |
disabled | boolean | false | Disabled 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
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | DividerOrientation | "horizontal" | horizontal · vertical |
ariaLabel | string | — | Accessible label |
Slots
| Slot | Description |
|---|---|
default | Label 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | T extends { id: string | number }[] | — | Items to render. Each must have a unique id. |
gap | number | 8 | Gap between items in px |
Slots
| Slot | Props | Description |
|---|---|---|
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
ResizeObserverto re-measure on container or content resize. - The
#itemslot must render a single root element per item for accurate width measurement. - Negative
gapvalues 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
| Prop | Type | Default | Description |
|---|---|---|---|
focusable | boolean | false | Become visible on focus (for skip links) |
as | string | "span" | Rendered element |
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-core | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.