packages/frontend/@n8n/design-system/src/v2/components/Loading/component-loading.md
Displays loading placeholders (skeleton screens) while content is being fetched or processed. Provides visual feedback to users that content is loading without showing a blank screen.
Props
animated?: boolean - Controls whether the skeleton shows pulsing animation. Default: truerows?: number - Number of skeleton rows to display. Default: 1cols?: number - Number of skeleton columns to display. When set (non-zero), overrides row-based layout. Default: 0shrinkLast?: boolean - Whether to shrink the last row to a shorter width. Only applies to 'h1' and 'p' variants when rows > 1. Default: truevariant?: SkeletonVariant - Visual variant determining the shape and style of skeleton items. Values: 'custom' | 'p' | 'text' | 'h1' | 'h3' | 'caption' | 'button' | 'image' | 'circle' | 'rect'. Default: 'p'Events
None - This is a purely presentational component.
Slots
None - Content is generated based on props.
Basic loading with rows:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<!-- Simple 3-row skeleton -->
<N8nLoading :rows="3" />
<!-- Conditional loading with 5 rows (use v-if for visibility control) -->
<N8nLoading v-if="isLoading" :rows="5" />
</template>
Variant-specific loading:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<!-- Heading skeleton -->
<N8nLoading variant="h1" />
<!-- Button skeleton -->
<N8nLoading variant="button" />
<!-- Paragraph skeleton with 3 rows -->
<N8nLoading variant="p" :rows="3" />
<!-- Rectangular shape -->
<N8nLoading variant="rect" />
<!-- Image placeholder -->
<N8nLoading variant="image" />
<!-- Text skeleton -->
<N8nLoading variant="text" />
<!-- Custom size (100% width/height) -->
<N8nLoading variant="custom" />
</template>
Without shrinking the last row:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<!-- All rows will be full width -->
<N8nLoading :rows="10" :shrink-last="false" />
</template>
Controlling animation:
<script setup lang="ts">
import { ref } from 'vue'
import { N8nLoading } from '@n8n/design-system'
const isLoading = ref(true)
const loadingRows = ref(5)
</script>
<template>
<!-- Animated paragraph skeleton -->
<N8nLoading v-if="isLoading" :rows="loadingRows" animated variant="p" />
<!-- Static text skeleton without animation -->
<N8nLoading :class="$style.loading" :animated="false" variant="text" />
</template>
Multiple instances for list items:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<div class="list">
<N8nLoading :rows="3" class="mb-xs" />
<N8nLoading :rows="3" class="mb-xs" />
<N8nLoading :rows="3" class="mb-xs" />
</div>
</template>
<style>
.mb-xs {
margin-bottom: var(--spacing--xs);
}
</style>
Conditional loading based on data availability:
<script setup lang="ts">
import { ref } from 'vue'
import { N8nLoading } from '@n8n/design-system'
const template = ref(null)
</script>
<template>
<div>
<!-- Show skeleton until data loads -->
<N8nLoading v-if="!template" :rows="1" variant="button" />
<N8nLoading v-if="!template?.name" :rows="2" variant="h1" />
<!-- Show actual content when loaded -->
<template v-if="template">
<h1>{{ template.name }}</h1>
<p>{{ template.description }}</p>
</template>
</div>
</template>
Column-based layout:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<!-- Renders 4 columns instead of rows (variant is ignored when cols is set) -->
<N8nLoading :cols="4" />
</template>
Custom styling with CSS classes:
<script setup lang="ts">
import { N8nLoading } from '@n8n/design-system'
</script>
<template>
<N8nLoading :rows="3" class="custom-loader" />
</template>
<style>
:global(.n8n-loading) div {
height: 32px;
width: 300px;
margin: 0;
}
:global(.n8n-loading) > div {
display: flex;
flex-direction: column;
gap: var(--spacing--2xs);
}
</style>