| name | n-skeleton |
| description | Skeleton component for displaying loading placeholders. Invoke when user needs to show content loading states with animated placeholders in Naive UI. |
| metadata | {"author":"jiaiyan","version":"1.0.0"} |
Naive UI Skeleton Component
Skeleton component for displaying animated loading placeholders while content is being loaded.
When to Use
Use this component when:
- Content loading: Display placeholders while data is being fetched
- Progressive loading: Show skeleton during lazy-loaded content
- Better UX: Reduce perceived loading time with animated placeholders
- Layout stability: Prevent layout shift during content load
When to Invoke
Invoke this skill when:
- User needs to show loading placeholders
- User wants to create skeleton screens
- User needs different skeleton shapes (text, box, circle)
- User wants to match skeleton size with actual components
- User asks about loading state UI
Features
- Text Skeleton: Animated text placeholders
- Shape Variants: Box, circle, and rounded shapes
- Size Options: small, medium, and large sizes
- Repeat Function: Repeat skeleton elements easily
- Animation Control: Enable or disable animation
- Custom Dimensions: Set custom width and height
API Reference
Skeleton Props
| Name | Type | Default | Description |
|---|
| text | boolean | false | Text skeleton style. |
| round | boolean | false | Round skeleton style. |
| circle | boolean | false | Circle skeleton style. |
| height | string | number | undefined | Skeleton height. |
| width | string | number | undefined | Skeleton width. |
| size | 'small' | 'medium' | 'large' | undefined | Skeleton size. |
| repeat | string | number | 1 | Repeat frequency for text skeletons. |
| animated | boolean | true | Whether to enable animation. |
| sharp | boolean | true | Whether to display as right-angled skeleton. |
Basic Usage
Text Skeleton
<template>
<n-skeleton text :repeat="2" />
<n-skeleton text style="width: 60%" />
</template>
Box Skeleton
<template>
<n-space vertical>
<n-skeleton height="40px" width="33%" />
<n-skeleton height="40px" width="66%" :sharp="false" />
<n-skeleton height="40px" round />
<n-skeleton height="40px" circle />
</n-space>
</template>
Size Matching
<template>
<n-space vertical>
<n-space>
<n-switch v-model:value="loading" />
Loading
</n-space>
<n-space>
<n-skeleton v-if="loading" :width="146" :sharp="false" size="medium" />
<n-button v-else>Won't you fly high</n-button>
<n-skeleton v-if="loading" :width="132" round size="medium" />
<n-button v-else round>free bird, yeah</n-button>
<n-skeleton v-if="loading" circle size="medium" />
<n-button v-else circle>?</n-button>
</n-space>
</n-space>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(true)
</script>
No Animation
<template>
<n-skeleton text :animated="false" :repeat="3" />
</template>
Common Patterns
Card Skeleton
<template>
<n-card v-if="loading" style="width: 300px;">
<n-skeleton text :repeat="2" />
<n-skeleton text style="width: 60%" />
<template #footer>
<n-skeleton text style="width: 100px" />
</template>
</n-card>
<n-card v-else style="width: 300px;" :title="data.title">
{{ data.content }}
<template #footer>
<n-button type="primary">Action</n-button>
</template>
</n-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
const loading = ref(true)
const data = reactive({
title: 'Card Title',
content: 'Card content goes here.'
})
</script>
List Skeleton
<template>
<n-list v-if="loading">
<n-list-item v-for="i in 3" :key="i">
<template #prefix>
<n-skeleton circle size="medium" />
</template>
<n-skeleton text style="width: 200px" />
</n-list-item>
</n-list>
<n-list v-else>
<n-list-item v-for="item in items" :key="item.id">
<template #prefix>
<n-avatar :src="item.avatar" />
</template>
{{ item.name }}
</n-list-item>
</n-list>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(true)
const items = ref([
{ id: 1, name: 'Item 1', avatar: '/avatar1.jpg' },
{ id: 2, name: 'Item 2', avatar: '/avatar2.jpg' },
{ id: 3, name: 'Item 3', avatar: '/avatar3.jpg' }
])
</script>
Table Skeleton
<template>
<n-data-table v-if="!loading" :columns="columns" :data="data" />
<div v-else class="table-skeleton">
<n-skeleton v-for="i in 5" :key="i" text :repeat="4" style="margin-bottom: 16px;" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(true)
const columns = []
const data = []
</script>
<style scoped>
.table-skeleton {
padding: 16px;
}
</style>
Article Skeleton
<template>
<div class="article-skeleton" v-if="loading">
<n-skeleton text style="width: 50%; height: 32px; margin-bottom: 24px;" />
<n-skeleton text :repeat="4" />
<n-skeleton text style="width: 70%" />
<n-skeleton height="200px" style="margin: 24px 0;" />
<n-skeleton text :repeat="3" />
<n-skeleton text style="width: 60%" />
</div>
<article v-else>
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
<img :src="article.image" alt="Article image" />
<p>{{ article.moreContent }}</p>
</article>
</template>
<script setup>
import { ref, reactive } from 'vue'
const loading = ref(true)
const article = reactive({
title: 'Article Title',
content: 'Article content...',
image: '/article.jpg',
moreContent: 'More content...'
})
</script>
Profile Card Skeleton
<template>
<n-card style="width: 300px;">
<div class="profile-skeleton" v-if="loading">
<n-skeleton circle :size="80" style="margin: 0 auto 16px;" />
<n-skeleton text style="width: 60%; margin: 0 auto 8px;" />
<n-skeleton text style="width: 80%; margin: 0 auto;" />
<n-divider />
<n-space justify="center">
<n-skeleton :width="80" :height="32" round />
<n-skeleton :width="80" :height="32" round />
</n-space>
</div>
<div class="profile" v-else>
<n-avatar :src="user.avatar" :size="80" style="margin: 0 auto 16px;" />
<h3 style="text-align: center;">{{ user.name }}</h3>
<p style="text-align: center; color: #999;">{{ user.bio }}</p>
<n-divider />
<n-space justify="center">
<n-button size="small">Follow</n-button>
<n-button size="small">Message</n-button>
</n-space>
</div>
</n-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
const loading = ref(true)
const user = reactive({
name: 'John Doe',
bio: 'Software Developer',
avatar: '/avatar.jpg'
})
</script>
Form Skeleton
<template>
<n-form v-if="!loading" :model="formData">
<n-form-item label="Name">
<n-input v-model:value="formData.name" />
</n-form-item>
<n-form-item label="Email">
<n-input v-model:value="formData.email" />
</n-form-item>
<n-button type="primary">Submit</n-button>
</n-form>
<div v-else class="form-skeleton">
<n-skeleton text style="width: 80px; margin-bottom: 8px;" />
<n-skeleton :height="34" style="margin-bottom: 16px;" />
<n-skeleton text style="width: 80px; margin-bottom: 8px;" />
<n-skeleton :height="34" style="margin-bottom: 16px;" />
<n-skeleton :width="100" :height="34" round />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const loading = ref(true)
const formData = reactive({
name: '',
email: ''
})
</script>
Grid Skeleton
<template>
<n-grid v-if="loading" :cols="3" :x-gap="16" :y-gap="16">
<n-gi v-for="i in 6" :key="i">
<n-card>
<n-skeleton height="150px" style="margin-bottom: 12px;" />
<n-skeleton text style="width: 70%; margin-bottom: 8px;" />
<n-skeleton text style="width: 50%;" />
</n-card>
</n-gi>
</n-grid>
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16">
<n-gi v-for="item in items" :key="item.id">
<n-card>
<img :src="item.image" style="width: 100%; height: 150px; object-fit: cover;" />
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</n-card>
</n-gi>
</n-grid>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(true)
const items = ref([])
</script>
Best Practices
-
Match skeleton dimensions: Size skeleton to match actual content
<n-skeleton :width="200" :height="34" /> <!-- Match button size -->
-
Use appropriate shapes: Match skeleton shape to content type
<n-skeleton circle /> <!-- For avatars -->
<n-skeleton text /> <!-- For text -->
-
Disable animation for static content: Turn off animation when not needed
<n-skeleton :animated="false" />
-
Use repeat for multiple lines: Efficiently create multiple text lines
<n-skeleton text :repeat="3" />
-
Maintain layout stability: Ensure skeleton matches final content layout
<n-skeleton style="width: 100%; height: 200px;" />
-
Use size prop for component matching: Match skeleton size with components
<n-skeleton size="small" /> <!-- Matches small components -->
-
Combine with loading states: Use with conditional rendering
<n-skeleton v-if="loading" />
<div v-else>{{ content }}</div>