Skip to content
Merged
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
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ function UserCard({ loading, user }) {

When `loading={true}`, children are hidden and a shimmer animation plays. When `loading={false}`, the shimmer fades out and children fade in.

### Multi-line skeleton (`GleamView.Line`)

Use `GleamView.Line` to create individual shimmer bars that inherit props from a parent `GleamView`. No conditional rendering — the wrapper pattern works for multi-line skeletons too.

```tsx
<GleamView loading={loading} speed={800} baseColor="#E0E0E0">
<GleamView.Line style={{ height: 22, borderRadius: 6, width: '70%' }}>
<Text style={{ fontSize: 16 }}>{title}</Text>
</GleamView.Line>
<GleamView.Line
style={{ height: 16, borderRadius: 4, width: '50%' }}
delay={100}
>
<Text style={{ fontSize: 13 }}>{subtitle}</Text>
</GleamView.Line>
</GleamView>
```

When `loading={true}`, each `GleamView.Line` renders its own shimmer bar, sized by `style`. The parent acts as a plain container (no block shimmer). When `loading={false}`, Lines become transparent and children render normally.

Lines inherit `loading`, `speed`, `direction`, `baseColor`, `highlightColor`, `intensity`, `transitionDuration`, and `transitionType` from the parent. `delay` and `onTransitionEnd` are per-line.

For best performance, place `GleamView.Line` as direct children of `GleamView` (or inside fragments). Lines nested inside intermediate wrappers (e.g., `<View>`) still work, but require an extra render cycle to detect.

Every `GleamView` provides context to its subtree. A `GleamView.Line` always binds to its nearest `GleamView` ancestor — nested `GleamView` components each control their own Lines independently.

### Staggered skeleton

```tsx
Expand Down Expand Up @@ -95,10 +121,21 @@ When `loading={true}`, children are hidden and a shimmer animation plays. When `
| `intensity` | `number` | `1` | Highlight strength (0-1). Lower = more subtle shimmer |
| `baseColor` | `string` | `#E0E0E0` | Background color of the shimmer |
| `highlightColor` | `string` | `#F5F5F5` | Color of the moving highlight |
| `onTransitionEnd` | `function` | — | Called when the fade transition completes. Receives `{ nativeEvent: { finished: boolean } }` |
| `onTransitionEnd` | `function` | — | Called when the transition completes or is interrupted. Receives `{ nativeEvent: { finished: boolean } }` — `true` if completed, `false` if interrupted (e.g., `loading` toggled back to `true`) |

All standard `View` props are also supported (`style`, `testID`, etc.). Note: the shimmer overlay supports uniform `borderRadius` only — per-corner radii are not applied to the shimmer.

### GleamView.Line Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `style` | `ViewStyle` | — | Style for the shimmer bar (height, width, borderRadius) |
| `delay` | `number` | `0` | Phase offset for this line (useful for stagger) |
| `onTransitionEnd` | `function` | — | Called when this line's transition completes |
| `testID` | `string` | — | Test identifier |

All standard accessibility props (`accessibilityLabel`, `accessibilityRole`, etc.) are accepted directly. Shimmer props (`loading`, `speed`, `direction`, etc.) cannot be passed to `GleamView.Line` — they are inherited automatically from the parent `GleamView`.

### GleamDirection

```tsx
Expand All @@ -121,7 +158,8 @@ GleamTransition.Collapse // 'collapse' — shimmer collapses vertically then ho

## Requirements

Comment thread
RamboWasReal marked this conversation as resolved.
- React Native **0.76+** (New Architecture / Fabric)
- React **19+**
- React Native **0.78+** (New Architecture / Fabric)
- iOS 15+
- Android SDK 24+

Expand All @@ -139,12 +177,20 @@ When `loading` switches to `false`:

1. The shimmer transitions out over `transitionDuration` ms (style depends on `transitionType`)
2. Children fade in simultaneously
3. `onTransitionEnd` fires when complete
3. `onTransitionEnd` fires with `finished: true` (or `finished: false` if interrupted)

All shimmer instances sharing the same `speed` are automatically synchronized via a shared clock.

The shimmer respects uniform `borderRadius` and standard view styles.

## Breaking changes (beta)

- When `GleamView.Line` children are present, the parent `GleamView` renders as a plain `View` container. `onTransitionEnd` on the parent is ignored in this mode — use `onTransitionEnd` on individual `GleamView.Line` components instead. A dev warning is emitted if this happens.

## Limitations

- The shimmer overlay supports uniform `borderRadius` only — per-corner radii are not applied to the shimmer.

## License

MIT
3 changes: 3 additions & 0 deletions android/src/main/java/com/gleam/GleamView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ class GleamView(context: Context) : ReactViewGroup(context) {

private fun applyLoadingState(wasLoading: Boolean) {
if (loading) {
if (isTransitioning) {
emitTransitionEnd(false)
}
// Set isTransitioning=false BEFORE cancel to prevent stale onAnimationEnd
isTransitioning = false
transitionGeneration++
Expand Down
78 changes: 78 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,39 @@ export default function App() {
</View>
</View>

{/* Multi-line skeleton using GleamView.Line */}
<Text style={styles.sectionLabel}>Multi-line (GleamView.Line)</Text>
<View style={styles.lineCard}>
<GleamView
loading={loading}
speed={speed}
intensity={intensity}
direction={direction}
baseColor={colors.base}
highlightColor={colors.highlight}
transitionDuration={transitionDuration}
transitionType={transitionType}
>
<GleamView.Line style={styles.lineTitle} delay={0}>
<Text style={styles.lineTitleText}>Article Title</Text>
</GleamView.Line>
<GleamView.Line style={styles.lineSubtitle} delay={100}>
<Text style={styles.lineSubtitleText}>
Published on March 15, 2026
</Text>
</GleamView.Line>
<GleamView.Line style={styles.lineBody} delay={200}>
<Text style={styles.lineBodyText}>
This is the first paragraph of the article content that wraps
across multiple lines.
</Text>
</GleamView.Line>
<GleamView.Line style={styles.lineBodyShort} delay={300}>
<Text style={styles.lineBodyText}>A shorter second paragraph.</Text>
</GleamView.Line>
</GleamView>
</View>

{/* Staggered demo */}
<Text style={styles.sectionLabel}>Staggered</Text>
<View style={styles.staggered}>
Expand Down Expand Up @@ -460,6 +493,51 @@ const styles = StyleSheet.create({
chipTextSelected: {
color: '#FFF',
},
lineCard: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
borderWidth: 1,
borderColor: '#E5E5E5',
padding: 16,
marginBottom: 20,
},
lineTitle: {
height: 24,
borderRadius: 6,
width: '75%',
marginBottom: 8,
},
lineTitleText: {
fontSize: 18,
fontWeight: '600',
color: '#222',
},
lineSubtitle: {
height: 16,
borderRadius: 4,
width: '50%',
marginBottom: 12,
},
lineSubtitleText: {
fontSize: 12,
color: '#999',
},
lineBody: {
height: 40,
borderRadius: 4,
width: '100%',
marginBottom: 6,
},
lineBodyShort: {
height: 20,
borderRadius: 4,
width: '60%',
},
lineBodyText: {
fontSize: 14,
color: '#555',
lineHeight: 20,
},
sectionLabel: {
fontSize: 13,
fontWeight: '600',
Expand Down
3 changes: 3 additions & 0 deletions ios/GleamView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

static void _startDisplayLinkIfNeeded(void) {
if (_displayLink) return;
_displayLink = [CADisplayLink displayLinkWithTarget:[GleamView class] selector:@selector(_onFrame:)];

Check warning on line 32 in ios/GleamView.mm

View workflow job for this annotation

GitHub Actions / build-ios

undeclared selector '_onFrame:' [-Wundeclared-selector]

Check warning on line 32 in ios/GleamView.mm

View workflow job for this annotation

GitHub Actions / build-ios

undeclared selector '_onFrame:' [-Wundeclared-selector]
if (@available(iOS 15.0, *)) {
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(30, 60, 60);
}
Expand Down Expand Up @@ -494,6 +494,9 @@
- (void)_applyLoadingState
{
if (_loading) {
if (_isTransitioning) {
[self _emitTransitionEnd:NO];
}
Comment on lines 496 to +499
_isTransitioning = NO;
_contentAlpha = 0.0;
_lastSetChildrenAlpha = -1.0;
Expand Down
5 changes: 5 additions & 0 deletions lib/module/GleamContext.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/module/GleamContext.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions lib/module/GleamLine.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/module/GleamLine.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 115 additions & 1 deletion lib/module/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/module/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading