React Native
The glass started life in an Expo app. The native items bring it back there — Liquid Glass on iOS 26 included.
native-glass picks the best surface the platform has: native Liquid Glass (expo-glass-effect) on iOS 26, a UIVisualEffect blur (expo-blur) on older iOS, backdrop-filter under react-native-web, and a translucent fill on Android. The palettes are the same six, as plain values.
pnpm dlx shadcn@latest https://glasscn.app/r/native-tokens.json https://glasscn.app/r/native-glass.json https://glasscn.app/r/native-aurora.json https://glasscn.app/r/native-press.jsonNeeds a components.json in the Expo project (as react-native-reusables uses) so the CLI knows your aliases. Files land in components/glass/native/.
Usage
import { View, Text } from "react-native"
import { Aurora } from "@/components/glass/native/aurora"
import { Glass } from "@/components/glass/native/glass"
import { Press } from "@/components/glass/native/press"
import { GlassThemeProvider, useGlassTheme } from "@/components/glass/native/tokens"
export default function Screen() {
return (
<GlassThemeProvider palette="ocean">
<View style={{ flex: 1 }}>
<Aurora />
<Card />
</View>
</GlassThemeProvider>
)
}
function Card() {
const t = useGlassTheme()
return (
<Press haptic>
<Glass style={{ margin: 20, padding: 20 }}>
<Text style={{ color: t.foreground, fontSize: 17, fontWeight: "600" }}>Tonight’s reflection</Text>
</Glass>
</Press>
)
}Two rules from shipping it
Liquid Glass won’t render under an ancestor whose opacity starts below 1. Fade entrances and screen transitions blank it — so on iOS 26, animate with translate and scale instead (canFade() from native-glass tells you when).
Animated drops Pressable’s function-style form, so Press takes a plain style.
Source
import * as React from "react"
import { useColorScheme } from "react-native"
/**
* glasscn tokens for React Native — the same names and palettes as the web
* registry's CSS variables, as plain values. Wrap your app in <GlassThemeProvider>
* to pick a palette or force a scheme; read with useGlassTheme().
*/
export type PaletteName = "dusk" | "ocean" | "rose" | "sage" | "amber" | "graphite"
export type Scheme = "light" | "dark"
const PALETTES: Record<PaletteName, { light: string; dark: string; aurora: { light: [string, string, string]; dark: [string, string, string] }; rings: [string, string] }> = {
dusk: { light: "#EE5A36", dark: "#FF7A55", aurora: { light: ["#FFB199", "#C7B2FF", "#FFD9AE"], dark: ["#9A3A2A", "#4A2F96", "#7A4A1C"] }, rings: ["#A77BFF", "#FFAA33"] },
ocean: { light: "#1F74F0", dark: "#4DA3FF", aurora: { light: ["#9CC6FF", "#A6EEE4", "#C7BCFF"], dark: ["#14408A", "#0E5E66", "#33298A"] }, rings: ["#2CCFC2", "#8E7CFF"] },
rose: { light: "#E0436B", dark: "#FF6B8E", aurora: { light: ["#FFB3C8", "#E2B8FF", "#FFD3DE"], dark: ["#8A2346", "#5E2C86", "#4A1F66"] }, rings: ["#B57BFF", "#FF9466"] },
sage: { light: "#23935F", dark: "#4CD08F", aurora: { light: ["#B2EACB", "#DCF2A8", "#A9DFEA"], dark: ["#1C6644", "#466620", "#145A66"] }, rings: ["#93CC3F", "#33B2D6"] },
amber: { light: "#C97A06", dark: "#FFB340", aurora: { light: ["#FFD898", "#FFBBA3", "#FFF0B3"], dark: ["#7A520F", "#7A321F", "#5E5612"] }, rings: ["#FF6F4F", "#E8C21C"] },
graphite: { light: "#1C1C1E", dark: "#F2F2F7", aurora: { light: ["#D3D8E2", "#E4DEF0", "#CCDFE8"], dark: ["#30343F", "#3A3446", "#24343E"] }, rings: ["#7C7C84", "#AEAEB4"] },
}
export interface GlassTheme {
scheme: Scheme
palette: PaletteName
foreground: string
mutedForeground: string
primary: string
primaryForeground: string
glass: string
glassStrong: string
/** Opaque-enough fill where there is no blur (Android). */
glassSolid: string
/** Tint laid into Liquid Glass so it reads on a pale aurora. */
glassTint: string
glassBorder: string
glassShadow: string
fill: string
fillStrong: string
auroraBase: string
aurora: [string, string, string]
chart: [string, string, string]
}
export function buildGlassTheme(scheme: Scheme, palette: PaletteName = "dusk"): GlassTheme {
const p = PALETTES[palette] ?? PALETTES.dusk
const dark = scheme === "dark"
const primary = dark ? p.dark : p.light
return {
scheme,
palette,
foreground: dark ? "#FFFFFF" : "#0B0B0F",
mutedForeground: dark ? "rgba(235,235,245,0.68)" : "rgba(60,60,67,0.74)",
primary,
primaryForeground: dark ? (palette === "graphite" ? "#000000" : "#1a0b05") : "#FFFFFF",
glass: dark ? "rgba(36,36,44,0.42)" : "rgba(255,255,255,0.52)",
glassStrong: dark ? "rgba(30,30,38,0.72)" : "rgba(255,255,255,0.74)",
glassSolid: dark ? "rgba(28,28,34,0.9)" : "rgba(255,255,255,0.88)",
glassTint: dark ? "rgba(30,30,38,0.4)" : "rgba(255,255,255,0.45)",
glassBorder: dark ? "rgba(255,255,255,0.12)" : "rgba(255,255,255,0.9)",
glassShadow: dark ? "rgba(0,0,0,0.5)" : "rgba(31,38,64,0.12)",
fill: dark ? "rgba(120,120,128,0.24)" : "rgba(120,120,128,0.12)",
fillStrong: dark ? "rgba(120,120,128,0.36)" : "rgba(120,120,128,0.2)",
auroraBase: dark ? "#050507" : "#EEF0F5",
aurora: dark ? p.aurora.dark : p.aurora.light,
chart: [primary, p.rings[0], p.rings[1]],
}
}
const GlassThemeContext = React.createContext<{ palette?: PaletteName; scheme?: Scheme } | null>(null)
export function GlassThemeProvider({ palette, scheme, children }: { palette?: PaletteName; scheme?: Scheme; children: React.ReactNode }) {
const value = React.useMemo(() => ({ palette, scheme }), [palette, scheme])
return <GlassThemeContext.Provider value={value}>{children}</GlassThemeContext.Provider>
}
/** The current tokens: the provider's palette/scheme, else Dusk and the system scheme. */
export function useGlassTheme(): GlassTheme {
const ctx = React.useContext(GlassThemeContext)
const system = useColorScheme()
const scheme: Scheme = ctx?.scheme ?? (system === "dark" ? "dark" : "light")
const palette = ctx?.palette ?? "dusk"
return React.useMemo(() => buildGlassTheme(scheme, palette), [scheme, palette])
}
import * as React from "react"
import { Platform, StyleSheet, View, type ViewProps } from "react-native"
import { BlurView } from "expo-blur"
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"
import { useGlassTheme } from "@/components/glass/native/tokens"
let liquid: boolean | null = null
/** Liquid Glass (iOS 26+) when the OS has it. Checked once. */
export function hasLiquidGlass() {
if (liquid === null) {
try {
liquid = Platform.OS === "ios" && isLiquidGlassAvailable()
} catch {
liquid = false
}
}
return liquid
}
/**
* Liquid Glass won't render under an ancestor whose opacity starts below 1 —
* entrances on iOS 26 should move (translate, scale) but not fade.
*/
export const canFade = () => !hasLiquidGlass()
export type GlassProps = ViewProps & {
radius?: number
/** Tint the glass (an accent wash) instead of the neutral frost. */
tint?: string
/** A soft shadow on web/Android; iOS glass carries its own depth. */
raised?: boolean
/** Liquid Glass reacts to touch — buttons, tab bars. */
interactive?: boolean
/** Top chrome that fades with scroll: never Liquid Glass (it must not be faded). */
bar?: boolean
}
/**
* Frosted glass for React Native.
* iOS 26: native Liquid Glass. iOS < 26: UIVisualEffect blur. Web: backdrop-filter. Android: a translucent fill.
*/
export function Glass({ radius = 26, tint, raised = true, interactive, bar, style, children, ...rest }: GlassProps) {
const t = useGlassTheme()
const shape = { borderRadius: radius, borderCurve: "continuous" as const }
if (!bar && hasLiquidGlass()) {
return (
<GlassView glassEffectStyle="regular" colorScheme={t.scheme} tintColor={tint ?? t.glassTint} isInteractive={interactive} style={[shape, style]} {...rest}>
{children}
</GlassView>
)
}
if (Platform.OS === "web") {
return (
<View
style={[
shape,
{
backgroundColor: tint ?? t.glass,
borderWidth: 1,
borderColor: t.glassBorder,
backdropFilter: "blur(28px) saturate(180%)",
WebkitBackdropFilter: "blur(28px) saturate(180%)",
boxShadow: raised ? `0 10px 30px ${t.glassShadow}, inset 0 1px 0 ${t.glassBorder}` : undefined,
} as object,
style,
]}
{...rest}
>
{children}
</View>
)
}
if (Platform.OS === "android") {
return (
<View style={[shape, { backgroundColor: tint ?? t.glassSolid, borderWidth: StyleSheet.hairlineWidth, borderColor: t.glassBorder, elevation: raised ? 2 : 0 }, style]} {...rest}>
{children}
</View>
)
}
return (
<View style={[shape, { overflow: "hidden" }, style]} {...rest}>
<BlurView
tint={bar ? (t.scheme === "dark" ? "systemChromeMaterialDark" : "systemChromeMaterialLight") : t.scheme === "dark" ? "systemUltraThinMaterialDark" : "systemUltraThinMaterialLight"}
intensity={80}
style={StyleSheet.absoluteFill}
/>
<View style={[StyleSheet.absoluteFill, shape, { pointerEvents: "none", backgroundColor: tint ?? t.glass, borderWidth: StyleSheet.hairlineWidth, borderColor: t.glassBorder }]} />
{children}
</View>
)
}
import * as React from "react"
import { AccessibilityInfo, Animated, Easing, StyleSheet, View, useWindowDimensions } from "react-native"
import { Circle, Defs, RadialGradient, Stop, Svg } from "react-native-svg"
import { useGlassTheme } from "@/components/glass/native/tokens"
const BLOBS = [
{ x: -0.25, y: -0.12, size: 1.25, dx: 40, dy: 30, ms: 17000 },
{ x: 0.45, y: 0.12, size: 1.05, dx: -36, dy: 44, ms: 21000 },
{ x: -0.1, y: 0.62, size: 1.2, dx: 30, dy: -38, ms: 19000 },
]
function useReducedMotion() {
const [reduced, setReduced] = React.useState(false)
React.useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled().then(setReduced).catch(() => {})
const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", setReduced)
return () => sub.remove()
}, [])
return reduced
}
function Blob({ color, i, w, h, dark, still }: { color: string; i: number; w: number; h: number; dark: boolean; still: boolean }) {
const id = React.useId().replace(/:/g, "")
const [t] = React.useState(() => new Animated.Value(0))
const b = BLOBS[i]
const size = Math.max(w, 380) * b.size
React.useEffect(() => {
if (still) return
const loop = Animated.loop(
Animated.sequence([
Animated.timing(t, { toValue: 1, duration: b.ms, easing: Easing.inOut(Easing.sin), useNativeDriver: true }),
Animated.timing(t, { toValue: 0, duration: b.ms, easing: Easing.inOut(Easing.sin), useNativeDriver: true }),
])
)
loop.start()
return () => loop.stop()
}, [t, still, b.ms])
return (
<Animated.View
style={{
pointerEvents: "none",
position: "absolute",
left: b.x * w,
top: b.y * h,
width: size,
height: size,
transform: [
{ translateX: t.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
{ translateY: t.interpolate({ inputRange: [0, 1], outputRange: [0, b.dy] }) },
{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [1, 1.08] }) },
],
}}
>
<Svg width={size} height={size}>
<Defs>
<RadialGradient id={`a${id}`} cx="50%" cy="50%" r="50%">
<Stop offset="0" stopColor={color} stopOpacity={dark ? 0.9 : 0.95} />
<Stop offset="0.45" stopColor={color} stopOpacity={dark ? 0.45 : 0.55} />
<Stop offset="1" stopColor={color} stopOpacity={0} />
</RadialGradient>
</Defs>
<Circle cx={size / 2} cy={size / 2} r={size / 2} fill={`url(#a${id})`} />
</Svg>
</Animated.View>
)
}
/** The living ground behind glass: three palette blobs drifting over the base. Fills its parent. */
export function Aurora() {
const t = useGlassTheme()
const still = useReducedMotion()
const { width, height } = useWindowDimensions()
return (
<View style={[StyleSheet.absoluteFill, { pointerEvents: "none", backgroundColor: t.auroraBase, overflow: "hidden" }]}>
{t.aurora.map((color, i) => (
<Blob key={i} i={i} color={color} w={width} h={height} dark={t.scheme === "dark"} still={still} />
))}
</View>
)
}
import * as React from "react"
import { Animated, Pressable, type GestureResponderEvent, type PressableProps, type StyleProp, type ViewStyle } from "react-native"
import * as Haptics from "expo-haptics"
const APressable = Animated.createAnimatedComponent(Pressable)
/**
* Every tappable thing: a soft spring squash, a slight dim, a selection haptic
* when asked, and a hover lift for pointers. Takes a plain style (Animated drops
* Pressable's function-style form).
*/
export function Press({
haptic = false,
squash = 0.97,
hover = 1,
style,
onPressIn,
onPressOut,
disabled,
...props
}: Omit<PressableProps, "style"> & { haptic?: boolean; squash?: number; hover?: number; style?: StyleProp<ViewStyle> }) {
const [scale] = React.useState(() => new Animated.Value(1))
const [opacity] = React.useState(() => new Animated.Value(1))
const spring = (v: Animated.Value, toValue: number) => Animated.spring(v, { toValue, friction: 8, tension: 380, useNativeDriver: true }).start()
return (
<APressable
disabled={disabled}
onPressIn={(e: GestureResponderEvent) => {
if (haptic && !disabled) Haptics.selectionAsync().catch(() => {})
spring(scale, squash)
Animated.timing(opacity, { toValue: 0.72, duration: 80, useNativeDriver: true }).start()
onPressIn?.(e)
}}
onPressOut={(e: GestureResponderEvent) => {
spring(scale, 1)
Animated.timing(opacity, { toValue: 1, duration: 160, useNativeDriver: true }).start()
onPressOut?.(e)
}}
onHoverIn={() => hover !== 1 && !disabled && spring(scale, hover)}
onHoverOut={() => hover !== 1 && spring(scale, 1)}
style={[style, { transform: [{ scale }], opacity }]}
{...props}
/>
)
}