Appearance
@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
| 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> |
CarrotLink
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
| 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".
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
| 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 |
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
| 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) |
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
| 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).
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
| Prop | Type | Default | Description |
|---|---|---|---|
focusable | boolean | false | Become visible on focus (for skip links) |
as | string | "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
| Prop | Type | Default | Description |
|---|---|---|---|
keys | string | — | Main key(s) — e.g. "S", "F11", "Enter" |
ctrl | boolean | false | Require Ctrl (or Cmd on Mac) |
shift | boolean | false | Require Shift |
alt | boolean | false | Require Alt (or Option on Mac) |
meta | boolean | false | Require Meta/Win (or Cmd on Mac) |
size | "sm" | "md" | "sm" | Render size |
listen | boolean | false | Listen for the shortcut and emit pressed |
preventDefault | boolean | true | Prevent default browser action when fired |
Events
| Event | Payload | Description |
|---|---|---|
pressed | KeyboardEvent | Shortcut was pressed (if listen is true) |
Dependencies
| Package | Purpose |
|---|---|
@carrot/design-components-core | CSS tokens + generated styles |
Build
bash
npm run buildBuilt 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>CarrotLink
Navigation links
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
Accessible icon button and skip link
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>