Appearance
@carrot/design-vue-spinners
Vue 3 loading indicators for the Carrot Design System. Spinners, spinner overlays, and skeleton placeholders.
Installation
ts
import { CarrotSpinner, CarrotSpinnerOverlay, CarrotSkeleton } from "@carrot/design-vue-spinners";Requires the CSS layer from @carrot/design-components-spinners.
Philosophy: These components provide base primitives and shape presets for loading states. Most teams will compose their own skeleton screens and loading patterns from these building blocks rather than using them directly — treat them as the foundation, not the finished article.
Components
CarrotSpinner
Animated loading spinner with optional label.
Usage
vue
<!-- Basic -->
<CarrotSpinner />
<!-- Sizes -->
<CarrotSpinner size="xs" />
<CarrotSpinner size="sm" />
<CarrotSpinner size="md" />
<CarrotSpinner size="lg" />
<CarrotSpinner size="xl" />
<!-- Colors -->
<CarrotSpinner color="accent" />
<CarrotSpinner color="muted" />
<CarrotSpinner color="white" />
<!-- With label -->
<CarrotSpinner label="Loading results…" />
<!-- Stacked label (below spinner) -->
<CarrotSpinner label="Please wait" stacked />
<!-- Custom aria-label -->
<CarrotSpinner aria-label="Fetching data" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | SpinnerSize | "md" | xs · sm · md · lg · xl |
color | SpinnerColor | "accent" | accent · muted · white |
label | string | — | Visible label text |
stacked | boolean | false | Stack label below spinner |
ariaLabel | string | "Loading" | Screen reader label (when no visible label) |
CarrotSpinnerOverlay
Full-container overlay with a centred spinner. Fades in/out with <Transition>.
Usage
vue
<!-- Basic (over a parent container) -->
<div style="position: relative">
<DataTable :rows="rows" />
<CarrotSpinnerOverlay :visible="isLoading" />
</div>
<!-- With label -->
<CarrotSpinnerOverlay :visible="isLoading" label="Saving changes…" />
<!-- Custom size/color -->
<CarrotSpinnerOverlay :visible="isLoading" size="xl" color="white" />
<!-- Custom content via slot -->
<CarrotSpinnerOverlay :visible="isLoading">
<CarrotProgressRing indeterminate />
<p>Processing your request…</p>
</CarrotSpinnerOverlay>Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | true | Show the overlay |
size | SpinnerSize | "lg" | Spinner size |
color | SpinnerColor | "accent" | Spinner color |
label | string | — | Label text |
stacked | boolean | true | Stack label below spinner |
The parent element should have position: relative for the overlay to fill it correctly.
CarrotSkeleton
Placeholder skeleton with shimmer animation for loading states.
Usage
vue
<!-- Single text line -->
<CarrotSkeleton />
<!-- Multiple text lines (last line 75% width for natural look) -->
<CarrotSkeleton :lines="3" />
<!-- Shapes -->
<CarrotSkeleton shape="text" />
<CarrotSkeleton shape="circle" />
<CarrotSkeleton shape="rect" />
<!-- Sizes -->
<CarrotSkeleton shape="rect" size="sm" />
<CarrotSkeleton shape="rect" size="md" />
<CarrotSkeleton shape="rect" size="lg" />
<!-- Custom dimensions -->
<CarrotSkeleton shape="rect" width="200px" height="120px" />
<!-- Static (no shimmer animation) -->
<CarrotSkeleton static />
<!-- Card skeleton example -->
<div style="display: flex; gap: 1rem; align-items: center">
<CarrotSkeleton shape="circle" size="lg" />
<div style="flex: 1">
<CarrotSkeleton width="60%" />
<CarrotSkeleton :lines="2" />
</div>
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
shape | SkeletonShape | "text" | text · circle · rect |
size | SkeletonSize | — | Height preset |
static | boolean | false | Disable shimmer animation |
width | string | — | Custom CSS width |
height | string | — | Custom CSS height |
lines | number | 1 | Number of text lines (text shape only) |
ariaLabel | string | "Loading" | Accessible label |
Accessibility
All components use role="status" for live region announcements. CarrotSpinnerOverlay additionally uses aria-live="polite". Screen-reader-only text is included via .sr-only spans when no visible label is present.
Dependencies
| Package | Purpose |
|---|---|
@carrot/design-components-spinners | 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 { CarrotSpinner, CarrotSpinnerOverlay, CarrotSkeleton } from "@carrot/design-vue-spinners";
import "@carrot/design-components-spinners";CarrotSpinner — Practical Examples
Inline Loading State
vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotSpinner } from "@carrot/design-vue-spinners";
const loading = ref(false);
async function save() {
loading.value = true;
await fetch("/api/save", { method: "POST" });
loading.value = false;
}
</script>
<template>
<button @click="save" :disabled="loading">
<CarrotSpinner v-if="loading" size="sm" color="white" />
<span v-else>Save</span>
</button>
</template>Full-Page Loading with Label
vue
<script setup lang="ts">
import { CarrotSpinner } from "@carrot/design-vue-spinners";
defineProps<{ visible: boolean }>();
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="page-loading">
<CarrotSpinner size="xl" label="Loading application…" stacked />
</div>
</Teleport>
</template>CarrotSpinnerOverlay — Practical Examples
Table Loading Overlay
vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotSpinnerOverlay } from "@carrot/design-vue-spinners";
const isLoading = ref(false);
</script>
<template>
<div style="position: relative">
<DataTable :rows="rows" />
<CarrotSpinnerOverlay :visible="isLoading" label="Refreshing data…" />
</div>
</template>CarrotSkeleton — Practical Examples
Article Card Skeleton
vue
<script setup lang="ts">
import { CarrotSkeleton } from "@carrot/design-vue-spinners";
defineProps<{ loading: boolean; article?: { title: string; excerpt: string } }>();
</script>
<template>
<div class="article-card">
<template v-if="loading">
<!-- Thumbnail -->
<CarrotSkeleton shape="rect" size="lg" />
<!-- Title -->
<CarrotSkeleton width="70%" />
<!-- Body lines -->
<CarrotSkeleton :lines="3" />
</template>
<template v-else>
<h2>{{ article?.title }}</h2>
<p>{{ article?.excerpt }}</p>
</template>
</div>
</template>User Profile Skeleton
vue
<template>
<div v-if="loading" style="display: flex; gap: 1rem; align-items: center">
<CarrotSkeleton shape="circle" size="lg" />
<div style="flex: 1">
<CarrotSkeleton width="40%" />
<CarrotSkeleton width="60%" />
</div>
</div>
<UserProfile v-else :user="user" />
</template>Common Patterns
List of Skeleton Rows
vue
<template>
<ul>
<li v-for="i in 5" :key="i" class="list-row">
<CarrotSkeleton shape="circle" />
<div style="flex: 1">
<CarrotSkeleton width="50%" />
<CarrotSkeleton width="80%" />
</div>
</li>
</ul>
</template>Static Skeleton (No Animation in Print/Reduced Motion)
vue
<template>
<CarrotSkeleton :static="prefersReducedMotion" shape="rect" size="md" />
</template>Conditional Overlay with Transition
The overlay already includes a built-in <Transition> fade. Simply toggle the visible prop — no extra wrapper needed:
vue
<template>
<div style="position: relative; min-height: 200px">
<ContentArea />
<CarrotSpinnerOverlay :visible="isSaving" size="md" label="Saving…" />
</div>
</template>