glasscn
Guides

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.json

Needs 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

app/index.tsx
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

components/glass/native/tokens.tsx
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])
}
components/glass/native/glass.tsx
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>
  )
}
components/glass/native/aurora.tsx
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>
  )
}
components/glass/native/press.tsx
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}
    />
  )
}