A React, Vue & Svelte shimmer/skeleton library that automatically adapts to your component's runtime structure. Unlike traditional shimmer libraries that require pre-defined skeleton structures, this library analyzes your actual component's DOM at runtime and generates a shimmer effect that perfectly matches its layout.
Traditional shimmer libraries require you to:
Shimmer From Structure eliminates all of that:
templatePropsnpm install shimmer-from-structure
# or
yarn add shimmer-from-structure
# or
pnpm add shimmer-from-structure
Shimmer From Structure provides dedicated packages for React and Vue.
React support is built-in to the main package for backward compatibility:
// React projects (or @shimmer-from-structure/react)
import { Shimmer } from 'shimmer-from-structure';
Vue support requires importing from the specific adapter:
// Vue 3 projects
import { Shimmer } from '@shimmer-from-structure/vue';
Svelte support is provided via its own adapter:
// Svelte projects
import { Shimmer } from '@shimmer-from-structure/svelte';
For components with hardcoded/static content:
import { Shimmer } from 'shimmer-from-structure';
function UserCard() {
return (
<Shimmer loading={isLoading}>
<div className="card">
<img src="avatar.jpg" className="avatar" />
<h2>John Doe</h2>
<p>Software Engineer</p>
</div>
</Shimmer>
);
}
<script setup>
import { ref } from 'vue';
import { Shimmer } from '@shimmer-from-structure/vue';
const isLoading = ref(true);
</script>
<template>
<Shimmer :loading="isLoading">
<div class="card">
<img src="avatar.jpg" class="avatar" />
<h2>John Doe</h2>
<p>Software Engineer</p>
</div>
</Shimmer>
</template>
<script>
import { Shimmer } from '@shimmer-from-structure/svelte';
let isLoading = true;
</script>
<Shimmer loading={isLoading}>
<div class="card">
<img src="avatar.jpg" class="avatar" />
<h2>John Doe</h2>
<p>Software Engineer</p>
</div>
</Shimmer>
templatePropsFor components that receive dynamic data via props, use templateProps to provide mock data for skeleton generation:
React
import { Shimmer } from 'shimmer-from-structure';
// Your component that accepts props
const UserCard = ({ user }) => (
<div className="card">
<img src={user.avatar} className="avatar" />
<h2>{user.name}</h2>
<p>{user.role}</p>
</div>
);
// Template data for the skeleton
const userTemplate = {
name: 'Loading...',
role: 'Loading role...',
avatar: 'placeholder.jpg',
};
function App() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
return (
<Shimmer loading={loading} templateProps={{ user: userTemplate }}>
<UserCard user={user || userTemplate} />
</Shimmer>
);
}
Vue
<script setup>
import { ref } from 'vue';
import { Shimmer } from '@shimmer-from-structure/vue';
import UserCard from './UserCard.vue';
const loading = ref(true);
const userTemplate = {
name: 'Loading...',
role: 'Loading role...',
avatar: 'placeholder.jpg',
};
</script>
<template>
<Shimmer :loading="loading" :templateProps="{ user: userTemplate }">
<UserCard :user="user || userTemplate" />
</Shimmer>
</template>
Svelte
<script>
import { Shimmer } from '@shimmer-from-structure/svelte';
import UserCard from './UserCard.svelte';
export let user;
let loading = true;
const userTemplate = {
name: 'Loading...',
role: 'Loading role...',
avatar: 'placeholder.jpg',
};
</script>
<Shimmer loading={loading} templateProps={{ user: userTemplate }}>
<UserCard user={user || userTemplate} />
</Shimmer>
The templateProps object is spread onto the first child component when loading, allowing it to render with mock data for measurement.
<Shimmer> Props| Prop | Type | Default | Description |
|---|---|---|---|
loading |
boolean |
true |
Whether to show shimmer effect or actual content |
children |
React.ReactNode |
required | The content to render/measure |
shimmerColor |
string |
'rgba(255,255,255,0.15)' |
Color of the shimmer wave |
backgroundColor |
string |
'rgba(255,255,255,0.08)' |
Background color of shimmer blocks |
duration |
number |
1.5 |
Animation duration in seconds |
fallbackBorderRadius |
number |
4 |
Border radius (px) for elements with no CSS border-radius |
templateProps |
Record<string, unknown> |
- | Props to inject into first child for skeleton rendering |
React
<Shimmer
loading={isLoading}
shimmerColor="rgba(255, 255, 255, 0.2)"
backgroundColor="rgba(255, 255, 255, 0.1)"
duration={2}
fallbackBorderRadius={8}
templateProps={{
user: userTemplate,
settings: settingsTemplate,
}}
>
<MyComponent user={user} settings={settings} />
</Shimmer>
Vue
<Shimmer
:loading="isLoading"
shimmerColor="rgba(255, 255, 255, 0.2)"
backgroundColor="rgba(255, 255, 255, 0.1)"
:duration="2"
:fallbackBorderRadius="8"
:templateProps="{
user: userTemplate,
settings: settingsTemplate,
}"
>
<MyComponent :user="user" :settings="settings" />
</Shimmer>
Svelte
<Shimmer
loading={isLoading}
shimmerColor="rgba(255, 255, 255, 0.2)"
backgroundColor="rgba(255, 255, 255, 0.1)"
duration={2}
fallbackBorderRadius={8}
templateProps={{
user: userTemplate,
settings: settingsTemplate,
}}
>
<MyComponent {user} {settings} />
</Shimmer>
loading={true}, your component renders with transparent text but visible container backgroundstemplateProps is provided, it's spread onto the first child so dynamic components can renderuseLayoutEffect to synchronously measure all leaf elements via getBoundingClientRect()border-radius from CSSopacity: 0, we use color: transparent so card backgrounds/borders show during loadingborder-radius: 0) use fallbackBorderRadius to avoid sharp rectanglesEach section can have its own independent loading state:
React
function Dashboard() {
const [loadingUser, setLoadingUser] = useState(true);
const [loadingStats, setLoadingStats] = useState(true);
return (
<>
{/* User profile section */}
<Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
<UserProfile user={user} />
</Shimmer>
{/* Stats section - with custom colors */}
<Shimmer
loading={loadingStats}
templateProps={{ stats: statsTemplate }}
shimmerColor="rgba(20, 184, 166, 0.2)"
>
<StatsGrid stats={stats} />
</Shimmer>
</>
);
}
Vue
<template>
<!-- User profile section -->
<Shimmer :loading="loadingUser" :templateProps="{ user: userTemplate }">
<UserProfile :user="user" />
</Shimmer>
<!-- Stats section - with custom colors -->
<Shimmer
:loading="loadingStats"
:templateProps="{ stats: statsTemplate }"
shimmerColor="rgba(20, 184, 166, 0.2)"
>
<StatsGrid :stats="stats" />
</Shimmer>
</template>
Svelte
<Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
<UserProfile {user} />
</Shimmer>
<Shimmer
loading={loadingStats}
templateProps={{ stats: statsTemplate }}
shimmerColor="rgba(20, 184, 166, 0.2)"
>
<StatsGrid {stats} />
</Shimmer>
React
<Shimmer loading={loadingTransactions} templateProps={{ transactions: transactionsTemplate }}>
<TransactionsList transactions={transactions} />
</Shimmer>
Vue
<Shimmer :loading="loadingTransactions" :templateProps="{ transactions: transactionsTemplate }">
<TransactionsList :transactions="transactions" />
</Shimmer>
Svelte
<Shimmer loading={loadingTransactions} templateProps={{ transactions: transactionsTemplate }}>
<TransactionsList {transactions} />
</Shimmer>
React
<Shimmer loading={loadingTeam} templateProps={{ members: teamTemplate }}>
<TeamMembers members={team} />
</Shimmer>
Vue
<Shimmer :loading="loadingTeam" :templateProps="{ members: teamTemplate }">
<TeamMembers :members="team" />
</Shimmer>
Svelte
<Shimmer loading={loadingTeam} templateProps={{ members: teamTemplate }}>
<TeamMembers members={team} />
</Shimmer>
Shimmer works seamlessly as a Suspense fallback. When used this way, loading is always true because React automatically unmounts the fallback and replaces it with the resolved component.
import { Suspense, lazy } from 'react';
import { Shimmer } from 'shimmer-from-structure';
const UserProfile = lazy(() => import('./UserProfile'));
function App() {
return (
<Suspense
fallback={
<Shimmer loading={true} templateProps={{ user: userTemplate }}>
<UserProfile />
</Shimmer>
}
>
<UserProfile userId="123" />
</Suspense>
);
}
loading={true} is Always SetWhen using Shimmer as a Suspense fallback:
loading={true}loadingMemoize the fallback to prevent re-renders:
const ShimmerFallback = React.memo(() => (
<Shimmer loading={true} templateProps={{ user: userTemplate }}>
<UserProfile />
</Shimmer>
));
// Usage
<Suspense fallback={<ShimmerFallback />}>
<UserProfile userId="123" />
</Suspense>;
Keep templates lightweight — the DOM is measured synchronously via useLayoutEffect, so avoid complex logic in your template.
You can set default configuration for your entire app (or specific sections) using the context/provider pattern. This is perfect for maintaining consistent themes without repeating props.
import { Shimmer, ShimmerProvider } from '@shimmer-from-structure/react';
function App() {
return (
// Set global defaults
<ShimmerProvider
config={{
shimmerColor: 'rgba(56, 189, 248, 0.4)', // Blue shimmer
backgroundColor: 'rgba(56, 189, 248, 0.1)', // Blue background
duration: 2.5,
fallbackBorderRadius: 8,
}}
>
<Dashboard />
</ShimmerProvider>
);
}
<!-- App.vue -->
<script setup>
import { provideShimmerConfig } from '@shimmer-from-structure/vue';
provideShimmerConfig({
shimmerColor: 'rgba(56, 189, 248, 0.4)',
backgroundColor: 'rgba(56, 189, 248, 0.1)',
duration: 2.5,
fallbackBorderRadius: 8,
});
</script>
<template>
<router-view />
</template>
<!-- App.svelte or any parent component -->
<script>
import { setShimmerConfig } from '@shimmer-from-structure/svelte';
setShimmerConfig({
shimmerColor: 'rgba(56, 189, 248, 0.4)',
backgroundColor: 'rgba(56, 189, 248, 0.1)',
duration: 2.5,
fallbackBorderRadius: 8,
});
</script>
<Dashboard />
Components inside the provider automatically inherit values. You can still override them locally:
React
// Inherits blue theme from provider
<Shimmer loading={true}><UserCard /></Shimmer>
// Overrides provider settings
<Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>
Vue
<!-- Inherits blue theme from provider -->
<Shimmer :loading="true"><UserCard /></Shimmer>
<!-- Overrides provider settings -->
<Shimmer :loading="true" :duration="0.5"><FastCard /></Shimmer>
Svelte
<!-- Inherits blue theme from provider -->
<Shimmer loading={true}><UserCard /></Shimmer>
<!-- Overrides provider settings -->
<Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>
If you need to access the current configuration in your own components:
React
import { useShimmerConfig } from 'shimmer-from-structure';
function MyComponent() {
const config = useShimmerConfig();
return <div style={{ background: config.backgroundColor }}>...</div>;
}
Vue
import { useShimmerConfig } from '@shimmer-from-structure/vue';
const config = useShimmerConfig();
console.log(config.value.backgroundColor);
Svelte
import { getShimmerConfig } from '@shimmer-from-structure/svelte';
const config = getShimmerConfig();
console.log(config.backgroundColor);
templateProps for Dynamic DataWhen your component receives data via props, always provide templateProps with mock data that matches the expected structure.
Ensure your template data has the same array length and property structure as real data for accurate shimmer layout.
Wrap each section in its own Shimmer for independent loading states:
// ✅ Good - independent loading
<Shimmer loading={loadingUsers}><UserList /></Shimmer>
<Shimmer loading={loadingPosts}><PostList /></Shimmer>
// ❌ Avoid - all-or-nothing loading
<Shimmer loading={loadingUsers || loadingPosts}>
<UserList />
<PostList />
</Shimmer>
Block elements like <h1>, <p> take full container width. If you want shimmer to match text width:
.title {
width: fit-content;
}
For async components (like charts), ensure containers have explicit dimensions so shimmer has something to measure.
Measurement happens only when loading changes to true
Uses useLayoutEffect for synchronous measurement (no flicker)
Minimal re-renders - only updates when loading state or children change
Lightweight DOM measurements using native browser APIs
Lightweight DOM measurements using native browser APIs
This is a monorepo managed with npm workspaces. Each package can be built independently:
# Install dependencies
npm install
# Build all packages
npm run build
# Build individual packages
npm run build:core
npm run build:react
npm run build:vue
npm run build:svelte
npm run build:main
# Run tests
npm test
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
ResponsiveContainer) may need explicit container dimensionsdisplay: none or zero dimensions won't be captured<svg> element is captured, not internal paths/shapesThis library is organized as a monorepo with four packages:
| Package | Description | Size |
|---|---|---|
@shimmer-from-structure/core |
Framework-agnostic DOM utilities | 1.44 kB |
@shimmer-from-structure/react |
React adapter | 12.84 kB |
@shimmer-from-structure/vue |
Vue 3 adapter | 3.89 kB |
@shimmer-from-structure/svelte |
Svelte adapter | 4.60 kB |
shimmer-from-structure |
Main package (React backward compatibility) | 0.93 kB |
The core package contains all DOM measurement logic, while React and Vue packages are thin wrappers that provide framework-specific APIs.
templatePropsMade with ❤️ for developers tired of maintaining skeleton screens