Skip to content

@carrot/design-vue-core

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

Installation

ts
import { CarrotPanel, CarrotLink, CarrotLabel, CarrotDivider, CarrotHidden, CarrotOverflowRow, CarrotShortcut } from "@carrot/design-vue-core";

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

Components

CarrotPanel

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

Usage

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

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

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

<!-- Flush (no padding) -->
<CarrotPanel flush>No padding</CarrotPanel>

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

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

<!-- Composing -->
<CarrotPanel variant="outlined" flush>
  <CarrotPanel size="md">Header area</CarrotPanel>
  <CarrotDivider />
  <CarrotPanel size="md">Body area</CarrotPanel>
</CarrotPanel>

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 -->
<CarrotLink href="/about">About</CarrotLink>

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

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

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

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

<!-- Active state -->
<CarrotLink href="/dashboard" active>Dashboard</CarrotLink>

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

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

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


CarrotLabel

Styled label with required indicator support.

Usage

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

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

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

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

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

Props

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

CarrotDivider

Horizontal or vertical separator with optional label.

Usage

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

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

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

Props

PropTypeDefaultDescription
orientationDividerOrientation"horizontal"horizontal · vertical
ariaLabelstringAccessible label

Slots

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

CarrotOverflowRow

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 -->
<CarrotOverflowRow :items="genres">
  <template #item="{ item }">
    <CarrotBadge :label="item.name" auto-color />
  </template>
</CarrotOverflowRow>

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

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

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

CarrotHidden

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

Usage

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

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

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

Props

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

CarrotShortcut

Keyboard shortcut badge that displays key combinations. Auto-detects platform (Cmd on Mac, Ctrl on others) and highlights when the keys are held down (data-active). Optionally listens for the shortcut and emits pressed.

Usage

vue
<!-- Display only -->
<CarrotShortcut keys="S" ctrl />

<!-- With Shift -->
<CarrotShortcut keys="Z" ctrl shift />

<!-- Listen and emit -->
<CarrotShortcut keys="S" ctrl listen @pressed="onSave" />

<!-- Medium size -->
<CarrotShortcut keys="F11" size="md" />

Props

PropTypeDefaultDescription
keysstringMain key(s) — e.g. "S", "F11", "Enter"
ctrlbooleanfalseRequire Ctrl (or Cmd on Mac)
shiftbooleanfalseRequire Shift
altbooleanfalseRequire Alt (or Option on Mac)
metabooleanfalseRequire Meta/Win (or Cmd on Mac)
size"sm" | "md""sm"Render size
listenbooleanfalseListen for the shortcut and emit pressed
preventDefaultbooleantruePrevent default browser action when fired

Events

EventPayloadDescription
pressedKeyboardEventShortcut was pressed (if listen is true)

Dependencies

PackagePurpose
@carrot/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 { CarrotPanel, CarrotLink, CarrotLabel, CarrotDivider, CarrotHidden, CarrotOverflowRow, CarrotShortcut } from "@carrot/design-vue-core";
import "@carrot/design-components-core";

CarrotPanel

Composing layout panels

vue
<script setup lang="ts">
import { CarrotPanel } from "@carrot/design-vue-core";
import { CarrotDivider } from "@carrot/design-vue-core";
</script>

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

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

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

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

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

CarrotOverflowRow

Tag overflow with badges

vue
<script setup lang="ts">
import { CarrotOverflowRow } from "@carrot/design-vue-core";
import { CarrotBadge } from "@carrot/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>
  <CarrotOverflowRow :items="tags" :gap="6">
    <template #item="{ item }">
      <CarrotBadge :label="item.label" auto-color size="sm" />
    </template>
    <template #overflow="{ hiddenCount }">
      <CarrotBadge :label="`+${hiddenCount} more`" variant="accent-subtle" size="sm" />
    </template>
  </CarrotOverflowRow>
</template>

CarrotDivider

Section separator and "or" divider

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

CarrotHidden

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

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

CarrotShortcut

Displaying keyboard shortcuts

vue
<script setup lang="ts">
import { CarrotShortcut } from "@carrot/design-vue-core";

function handleSave(e: KeyboardEvent) {
  console.log("Save triggered", e);
}
</script>

<template>
  <!-- Display-only shortcut badge -->
  <span>Save <CarrotShortcut keys="S" ctrl /></span>

  <!-- Active listener that highlights when held and emits on press -->
  <CarrotShortcut keys="S" ctrl listen @pressed="handleSave" />

  <!-- Multi-modifier -->
  <CarrotShortcut keys="Z" ctrl shift />
</template>

Common Patterns

Form field with label

vue
<script setup lang="ts">
import { CarrotLabel } from "@carrot/design-vue-core";
import { CarrotInput } from "@carrot/design-vue-inputs";
import { ref } from "vue";

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

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

Carrot