suiss/uim · React Native Component
Skeleton
A pulsing placeholder shape shown while content loads.
Skeleton is an open-source mobile component from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.


import { Skeleton } from "@/components/ui/skeleton"
export function SkeletonExample() {
return (
<Skeleton className="h-4 w-48" />
)
}import EsuissUI
import SwiftUI
struct SkeletonExample: View {
var body: some View {
Skeleton()
.frame(width: 192, height: 16)
}
}import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Skeleton
@Composable
fun SkeletonExample() {
Skeleton(Modifier.width(192.dp).height(16.dp))
}Skeleton is a Reanimated View with a rounded background whose opacity pulses between 1 and 0.5 on a loop. It has no fixed size; you shape it with classes to match the content it stands in for.
Installation
$ pnpm dlx @esuiss/uim@latest add skeleton$ pnpm dlx @esuiss/uim@latest add skeleton --platform swift$ pnpm dlx @esuiss/uim@latest add skeleton --platform kotlinManual
- Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets- Copy the source into
components/ui/skeleton.tsx:
import { cn } from "cn"
import * as React from "react"
import type { View } from "react-native"
import Animated, {
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"
const duration = 1000
function Skeleton({
className,
...props
}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets- Copy the source into
components/ui/skeleton.tsx:
import { cn } from "cn"
import * as React from "react"
import type { View } from "react-native"
import Animated, {
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"
const duration = 1000
function Skeleton({
className,
...props
}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {- Update the import paths to match your project.
- Install the dependencies:
npx expo install react-native-reanimated react-native-worklets- Copy the source into
components/ui/skeleton.tsx:
import * as React from "react"
import { StyleSheet, type View } from "react-native"
import Animated, {
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"
import { radius, useTheme } from "@/components/ui/theme"
const duration = 1000
const styles = StyleSheet.create({
root: { borderRadius: radius.md },
})- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Copy the theme files every view uses:
import SwiftUI
import UIKit
struct RGBA: Sendable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat
init(_ hex: UInt32, _ alpha: CGFloat = 1) {
red = CGFloat((hex >> 16) & 0xFF) / 255
green = CGFloat((hex >> 8) & 0xFF) / 255
blue = CGFloat(hex & 0xFF) / 255
self.alpha = alpha
}import SwiftUI
extension Color {
public static let surface = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
public static let surfaceSecondary = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
public static let surfaceTertiary = Color.adaptive(light: .init(0xE8E8ED), dark: .init(0x2C2C2E))
public static let surfaceRaised = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
public static let label = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let labelSecondary = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
public static let labelTertiary = Color.adaptive(light: .init(0x86868B), dark: .init(0x86868B))
public static let labelQuaternary = Color.adaptive(light: .init(0xD2D2D7), dark: .init(0x424245))
public static let control = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
public static let controlPressed = Color.adaptive(light: .init(0x000000, 0.12), dark: .init(0xFFFFFF, 0.22))
public static let controlSelected = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let controlDisabled = Color.adaptive(light: .init(0x000000, 0.03), dark: .init(0xFFFFFF, 0.05))
public static let onControlSelected = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))- Copy the source:
import SwiftUI
public struct Skeleton: View {
let cornerRadius: CGFloat
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var dimmed = false
public init(cornerRadius: CGFloat = Radius.md) {
self.cornerRadius = cornerRadius
}
public var body: some View {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(Color.surfaceSecondary)
.opacity(dimmed ? 0.5 : 1)- Add the files to your app target.
- Set
minSdkto 24 or higher and add the dependencies:
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")- Copy the theme files every view uses:
package com.esuiss.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffsetpackage com.esuiss.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Immutable
data class Colors(
val surface: Color,
val surfaceSecondary: Color,
val surfaceTertiary: Color,
val surfaceRaised: Color,
val label: Color,
val labelSecondary: Color,
val labelTertiary: Color,
val labelQuaternary: Color,- Copy the source and every file it uses:
package com.esuiss.ui.components
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayerpackage com.esuiss.ui.components
import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size- Update the
com.esuiss.uipackage names to match your project.
Usage
import { Skeleton } from "@/components/ui/skeleton"<Skeleton className="h-4 w-48" />Set width, height and radius through className, for example h-4 w-48 for a line of text or size-12 rounded-full for an avatar. Compose several skeletons to mirror the layout of the loaded content, then swap them for the real content once it arrives.
Examples
Profile
An avatar circle with two lines of text.


import { View } from "react-native"
import { Skeleton } from "@/components/ui/skeleton"
export function SkeletonProfileExample() {
return (
<View className="flex-row items-center gap-4">
<Skeleton className="size-12 rounded-full" />
<View className="gap-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-4 w-36" />
</View>
</View>
)
}import EsuissUI
import SwiftUI
struct SkeletonProfileExample: View {
var body: some View {
HStack(spacing: 16) {
Skeleton(cornerRadius: Radius.full)
.frame(width: 48, height: 48)
VStack(alignment: .leading, spacing: 8) {
Skeleton().frame(width: 192, height: 16)
Skeleton().frame(width: 144, height: 16)
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Skeleton
@Composable
fun SkeletonProfile() {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Skeleton(Modifier.size(48.dp), shape = CircleShape)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Skeleton(Modifier.width(192.dp).height(16.dp))
Skeleton(Modifier.width(144.dp).height(16.dp))
}
}
}Card
A media block with two lines of text below it.


import { View } from "react-native"
import { Skeleton } from "@/components/ui/skeleton"
export function SkeletonCardExample() {
return (
<View className="gap-3">
<Skeleton className="h-32 w-full rounded-xl" />
<View className="gap-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</View>
</View>
)
}import EsuissUI
import SwiftUI
struct SkeletonCardExample: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Skeleton(cornerRadius: Radius.xl)
.frame(height: 128)
.frame(maxWidth: .infinity)
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 8) {
Skeleton().frame(width: proxy.size.width * 0.75, height: 16)
Skeleton().frame(width: proxy.size.width * 0.5, height: 16)
}
}
.frame(height: 40)
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Skeleton
import com.esuiss.ui.theme.Radius
@Composable
fun SkeletonCard() {
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Skeleton(Modifier.fillMaxWidth().height(128.dp), shape = RoundedCornerShape(Radius.xl))
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Skeleton(Modifier.fillMaxWidth(0.75f).height(16.dp))
Skeleton(Modifier.fillMaxWidth(0.5f).height(16.dp))
}
}
}List
Rows with a thumbnail and two lines of text.


import { View } from "react-native"
import { Skeleton } from "@/components/ui/skeleton"
export function SkeletonListExample() {
return (
<View className="gap-4">
{["one", "two", "three", "four"].map((key) => (
<View key={key} className="flex-row items-center gap-3">
<Skeleton className="size-10 rounded-md" />
<View className="flex-1 gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-1/3" />
</View>
</View>
))}
</View>
)
}import EsuissUI
import SwiftUI
struct SkeletonListExample: View {
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(["one", "two", "three", "four"], id: \.self) { _ in
HStack(spacing: 12) {
Skeleton(cornerRadius: Radius.md)
.frame(width: 40, height: 40)
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 8) {
Skeleton().frame(width: proxy.size.width * 2 / 3, height: 16)
Skeleton().frame(width: proxy.size.width / 3, height: 12)
}
.frame(maxHeight: .infinity)
}
.frame(height: 40)
}
}
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Skeleton
import com.esuiss.ui.theme.Radius
@Composable
fun SkeletonList() {
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
repeat(4) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
Skeleton(Modifier.size(40.dp), shape = RoundedCornerShape(Radius.md))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Skeleton(Modifier.fillMaxWidth(2f / 3f).height(16.dp))
Skeleton(Modifier.fillMaxWidth(1f / 3f).height(12.dp))
}
}
}
}
}API Reference
Skeleton
React.ComponentProps<typeof View>React.RefAttributes<View>Accessibility
- The skeleton is a plain view with no role or label. If loading state matters to the user, announce it elsewhere, for example with a
Spinnerthat has anaccessibilityLabel.
Notes
- Uses the
bg-surface-secondarytoken. - The pulse runs on the UI thread through Reanimated, with a 1000 ms cycle.
- Light and dark colors follow the system appearance.