Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions skeleton-loader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SkeletonLoader
An animated placeholder component that displays shimmer-loading shapes while content loads.

## Getting Started

Install dependencies:
```bash
npm install
```

Share the component to your Webflow workspace:
```bash
npx webflow library share
```

For local development:
```bash
npm run dev
```

## Designer Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| ID | Id | — | HTML ID attribute for targeting |
| Variant | Variant | text | Primary skeleton shape type (text, circular, rectangular) |
| Text Line Count | Number | 3 | Number of text lines to display (1-6) |
| Width | Text | 100% | Width of rectangular or circular shape (CSS value) |
| Height | Text | 200px | Height of rectangular shape (CSS value) |
| Circle Size | Text | 64px | Diameter of circular shape (CSS value) |
| Show Circle | Visibility | — | Show or hide circular avatar placeholder |
| Show Rectangle | Visibility | — | Show or hide rectangular image placeholder |
| Show Text Lines | Visibility | — | Show or hide text line placeholders |
| Enable Animation | Boolean | true | Enable or disable shimmer animation |

## Styling

This component automatically adapts to your Webflow site's design system through site variables and inherited properties.

### Site Variables

To match your site's design system, define these CSS variables in your Webflow project settings. The component will use the fallback values shown below until you configure them.

| Site Variable | What It Controls | Fallback |
|---------------|------------------|----------|
| --background-secondary | Skeleton base color | #f5f5f5 |
| --border-color | Shimmer highlight color | #e5e5e5 |
| --border-radius | Corner rounding for shapes | 8px |

### Inherited Properties

The component inherits these CSS properties from its parent element:
- `font-family` — Typography style
- `color` — Text color
- `line-height` — Text spacing

## Extending in Code

### Creating a Card Skeleton

Combine multiple shape types to create a realistic card loading state:

```javascript
// Set variant to "text" and configure visibility props:
// - Show Rectangle: true (for card image)
// - Show Circle: true (for avatar)
// - Show Text Lines: true (for content)
// - Text Line Count: 4
// - Height: 180px (for card image)
// - Circle Size: 48px (for small avatar)
```

### Custom Loading States

Disable animation for a static placeholder or adjust timing:

```css
/* Slower shimmer animation */
.wf-skeletonloader-animated .wf-skeletonloader-circle::after,
.wf-skeletonloader-animated .wf-skeletonloader-rectangle::after,
.wf-skeletonloader-animated .wf-skeletonloader-text-line::after {
animation-duration: 3s;
}

/* Custom pulse timing */
.wf-skeletonloader:not(.wf-skeletonloader-animated) .wf-skeletonloader-circle {
animation-duration: 1.5s;
}
```

## Dependencies

No external dependencies.
17 changes: 17 additions & 0 deletions skeleton-loader/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SkeletonLoader</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { color: inherit; }
h1, h2, h3, h4, h5, h6 { color: inherit; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
5 changes: 5 additions & 0 deletions skeleton-loader/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Skeleton Loader",
"description": "Animated placeholder shapes for loading content states",
"category": "Feedback"
}
25 changes: 25 additions & 0 deletions skeleton-loader/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "skeleton-loader",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.3",
"@webflow/data-types": "^1.0.1",
"@webflow/react": "^1.0.1",
"@webflow/webflow-cli": "^1.8.44",
"typescript": "~5.8.3",
"vite": "^7.1.7"
}
}
1 change: 1 addition & 0 deletions skeleton-loader/screenshot-brand.b64

Large diffs are not rendered by default.

Binary file added skeleton-loader/screenshot-brand.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions skeleton-loader/screenshot-dark.b64

Large diffs are not rendered by default.

Binary file added skeleton-loader/screenshot-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions skeleton-loader/screenshot-light.b64

Large diffs are not rendered by default.

Binary file added skeleton-loader/screenshot-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions skeleton-loader/src/components/SkeletonLoader/SkeletonLoader.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Webflow Site Variables Used:
* - --background-secondary: Skeleton base color
* - --border-color: Shimmer highlight color
* - --border-radius: Corner rounding for shapes
*/

/* Box sizing reset */
.wf-skeletonloader *,
.wf-skeletonloader *::before,
.wf-skeletonloader *::after {
box-sizing: border-box;
}

/* Root element - inherit Webflow typography + default padding */
.wf-skeletonloader {
font-family: inherit;
color: inherit;
line-height: inherit;
padding: 24px;
--wf-skeletonloader-width: 100%;
--wf-skeletonloader-height: 200px;
--wf-skeletonloader-circle-size: 64px;
--wf-skeletonloader-line-width: 100%;
}

/* Shimmer animation keyframes */
@keyframes wf-skeletonloader-shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

@keyframes wf-skeletonloader-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}

/* Base skeleton shape styles */
.wf-skeletonloader-circle,
.wf-skeletonloader-rectangle,
.wf-skeletonloader-text-line {
background: var(--background-secondary, #f5f5f5);
position: relative;
overflow: hidden;
}

/* Shimmer effect overlay */
.wf-skeletonloader-animated .wf-skeletonloader-circle::after,
.wf-skeletonloader-animated .wf-skeletonloader-rectangle::after,
.wf-skeletonloader-animated .wf-skeletonloader-text-line::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
var(--border-color, #e5e5e5) 50%,
transparent 100%
);
animation: wf-skeletonloader-shimmer 2s infinite;
}

/* Pulse animation for non-animated state */
.wf-skeletonloader:not(.wf-skeletonloader-animated) .wf-skeletonloader-circle,
.wf-skeletonloader:not(.wf-skeletonloader-animated) .wf-skeletonloader-rectangle,
.wf-skeletonloader:not(.wf-skeletonloader-animated) .wf-skeletonloader-text-line {
animation: wf-skeletonloader-pulse 2s ease-in-out infinite;
}

/* Circular skeleton */
.wf-skeletonloader-circle {
width: var(--wf-skeletonloader-circle-size);
height: var(--wf-skeletonloader-circle-size);
border-radius: 50%;
margin-bottom: 16px;
}

/* Rectangular skeleton */
.wf-skeletonloader-rectangle {
width: var(--wf-skeletonloader-width);
height: var(--wf-skeletonloader-height);
border-radius: var(--border-radius, 8px);
margin-bottom: 16px;
}

/* Text lines container */
.wf-skeletonloader-text-container {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}

/* Individual text line */
.wf-skeletonloader-text-line {
height: 16px;
width: var(--wf-skeletonloader-line-width);
border-radius: var(--border-radius, 8px);
}
74 changes: 74 additions & 0 deletions skeleton-loader/src/components/SkeletonLoader/SkeletonLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from "react";

export interface SkeletonLoaderProps {
id?: string;
variant?: "text" | "circular" | "rectangular";
textLineCount?: number;
width?: string;
height?: string;
circleSize?: string;
showCircle?: boolean;
showRectangle?: boolean;
showTextLines?: boolean;
enableAnimation?: boolean;
}

export default function SkeletonLoader({
id,
variant = "text",
textLineCount = 3,
width = "100%",
height = "200px",
circleSize = "64px",
showCircle,
showRectangle,
showTextLines,
enableAnimation = true,
}: SkeletonLoaderProps) {
const clampedLineCount = Math.max(1, Math.min(6, textLineCount));

const lineWidths = ["100%", "95%", "90%", "85%", "92%", "88%"];

const shouldShowCircle = showCircle !== undefined ? showCircle : variant === "circular";
const shouldShowRectangle = showRectangle !== undefined ? showRectangle : variant === "rectangular";
const shouldShowTextLines = showTextLines !== undefined ? showTextLines : variant === "text";

return (
<div
id={id}
className={`wf-skeletonloader ${enableAnimation ? "wf-skeletonloader-animated" : ""}`}
style={
{
"--wf-skeletonloader-width": width,
"--wf-skeletonloader-height": height,
"--wf-skeletonloader-circle-size": circleSize,
} as React.CSSProperties
}
>
{shouldShowCircle && (
<div className="wf-skeletonloader-circle" aria-hidden="true"></div>
)}

{shouldShowRectangle && (
<div className="wf-skeletonloader-rectangle" aria-hidden="true"></div>
)}

{shouldShowTextLines && (
<div className="wf-skeletonloader-text-container">
{Array.from({ length: clampedLineCount }).map((_, index) => (
<div
key={index}
className="wf-skeletonloader-text-line"
style={
{
"--wf-skeletonloader-line-width": lineWidths[index % lineWidths.length],
} as React.CSSProperties
}
aria-hidden="true"
></div>
))}
</div>
)}
</div>
);
}
Loading