Skip to content

@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

PropTypeDefaultDescription
sizeSpinnerSize"md"xs · sm · md · lg · xl
colorSpinnerColor"accent"accent · muted · white
labelstringVisible label text
stackedbooleanfalseStack label below spinner
ariaLabelstring"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

PropTypeDefaultDescription
visiblebooleantrueShow the overlay
sizeSpinnerSize"lg"Spinner size
colorSpinnerColor"accent"Spinner color
labelstringLabel text
stackedbooleantrueStack 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

PropTypeDefaultDescription
shapeSkeletonShape"text"text · circle · rect
sizeSkeletonSizeHeight preset
staticbooleanfalseDisable shimmer animation
widthstringCustom CSS width
heightstringCustom CSS height
linesnumber1Number of text lines (text shape only)
ariaLabelstring"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

PackagePurpose
@carrot/design-components-spinnersCSS 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 { 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>

Carrot