Veltrix

skia shaders

Aurora Border

A low-frequency luminous border whose palette responds to semantic state.

AppleiOSAndroidAndroidExpoExpo Go
skiareanimated
GitHubOpen in GitHub

Installation

Install the dependencies, then copy the files for your preferred renderer.

pnpm add @shopify/react-native-skia react-native-reanimated

Install tailwind variants

pnpm add tailwind-variants

Usage

AuroraBorderExample.tsx
import { Text, View } from "react-native";import { AuroraBorder } from "@animations/ui/components/skia-shaders/aurora-border/uniwind";export function AuroraBorderExample() {  return (    <AuroraBorder      classNames={{ content: "px-6" }}      state="success"    >      <View className="p-6">        <Text className="text-xl font-extrabold">System online</Text>      </View>    </AuroraBorder>  );}

Component files

6 files

aurora-border/aurora-layer.tsx
import {  BlurMask,  Canvas,  RoundedRect,  SweepGradient,  useCanvasSize,} from "@shopify/react-native-skia";import { StyleSheet } from "react-native";import {  AURORA_BORDER_CONFIG,  AURORA_BORDER_PALETTES,} from "./config";import type { AuroraBorderBaseProps } from "./types";import { useAuroraBorder } from "./use-aurora";type AuroraLayerProps = Pick<  AuroraBorderBaseProps,  | "borderRadius"  | "borderWidth"  | "colors"  | "duration"  | "intensity"  | "paused"  | "state">;export function AuroraLayer({  borderRadius = AURORA_BORDER_CONFIG.borderRadius,  borderWidth = AURORA_BORDER_CONFIG.borderWidth,  colors,  duration = AURORA_BORDER_CONFIG.duration,  intensity = AURORA_BORDER_CONFIG.intensity,  paused = false,  state = "active",}: AuroraLayerProps) {  const {    ref,    size: { height: canvasHeight, width: canvasWidth },  } = useCanvasSize();  const {    center,    glowOpacity,    height,    inset,    radius,    transform,    width,  } = useAuroraBorder({    borderRadius,    borderWidth,    duration,    height: canvasHeight,    intensity,    paused,    width: canvasWidth,  });  const palette = colors ?? AURORA_BORDER_PALETTES[state];  return (    <Canvas      pointerEvents="none"      ref={ref}      style={StyleSheet.absoluteFill}    >      <RoundedRect        height={height}        opacity={glowOpacity * 0.7}        r={radius}        strokeWidth={borderWidth * 2.5}        style="stroke"        width={width}        x={inset}        y={inset}      >        <SweepGradient          c={center}          colors={palette}          origin={center}          transform={transform}        />        <BlurMask          blur={AURORA_BORDER_CONFIG.glowBlur * intensity}          style="solid"        />      </RoundedRect>      <RoundedRect        height={height}        r={radius}        strokeWidth={borderWidth}        style="stroke"        width={width}        x={inset}        y={inset}      >        <SweepGradient          c={center}          colors={palette}          origin={center}          transform={transform}        />      </RoundedRect>    </Canvas>  );}
aurora-border/config.ts
import type { AuroraBorderState } from "./types";export const AURORA_BORDER_CONFIG = {  borderRadius: 24,  borderWidth: 2,  duration: 9_000,  glowBlur: 9,  intensity: 1,} as const;export const AURORA_BORDER_PALETTES = {  active: ["#7c3aed", "#22d3ee", "#f472b6", "#a78bfa", "#7c3aed"],  idle: ["#5b6475", "#64748b", "#8b5cf6", "#475569", "#5b6475"],  success: ["#059669", "#2dd4bf", "#a3e635", "#22d3ee", "#059669"],  warning: ["#f59e0b", "#fb7185", "#f97316", "#fde047", "#f59e0b"],} satisfies Record<AuroraBorderState, string[]>;
aurora-border/types.ts
import type { ReactNode } from "react";export type AuroraBorderState =  | "active"  | "idle"  | "success"  | "warning";export type AuroraBorderBaseProps = {  accessibilityLabel?: string;  borderRadius?: number;  borderWidth?: number;  children: ReactNode;  colors?: string[];  duration?: number;  intensity?: number;  paused?: boolean;  state?: AuroraBorderState;};
aurora-border/uniwind/index.tsx
import { View } from "react-native";import type { SlotsToClasses } from "#shared/types/slots";import { AuroraLayer } from "../aurora-layer";import type { AuroraBorderBaseProps } from "../types";import {  auroraBorderVariants,  type AuroraBorderSlot,} from "./variants";export type AuroraBorderProps = AuroraBorderBaseProps & {  className?: string;  classNames?: SlotsToClasses<AuroraBorderSlot>;};export function AuroraBorder({  accessibilityLabel,  borderRadius,  borderWidth,  children,  className,  classNames,  colors,  duration,  intensity,  paused,  state,}: AuroraBorderProps) {  const { container, content } = auroraBorderVariants();  return (    <View      accessibilityLabel={accessibilityLabel}      accessible={accessibilityLabel !== undefined}      className={container({        className: [className, classNames?.container],      })}      style={borderRadius !== undefined && { borderRadius }}    >      <View        className={content({          className: classNames?.content,        })}      >        {children}      </View>      <AuroraLayer        borderRadius={borderRadius}        borderWidth={borderWidth}        colors={colors}        duration={duration}        intensity={intensity}        paused={paused}        state={state}      />    </View>  );}
aurora-border/uniwind/variants.ts
import { tv } from "tailwind-variants";export const auroraBorderSlots = {  container: "overflow-hidden rounded-3xl",  content: "",} as const;export type AuroraBorderSlot = keyof typeof auroraBorderSlots;export const auroraBorderVariants = tv({  slots: auroraBorderSlots,});
aurora-border/use-aurora.ts
import { useClock } from "@shopify/react-native-skia";import {  useDerivedValue,  useReducedMotion,} from "react-native-reanimated";type UseAuroraBorderOptions = {  borderRadius: number;  borderWidth: number;  duration: number;  height: number;  intensity: number;  paused: boolean;  width: number;};export function useAuroraBorder({  borderRadius,  borderWidth,  duration,  height: canvasHeight,  intensity,  paused,  width: canvasWidth,}: UseAuroraBorderOptions) {  const clock = useClock();  const reducedMotion = useReducedMotion();  const inset = borderWidth / 2;  const width = Math.max(0, canvasWidth - inset * 2);  const height = Math.max(0, canvasHeight - inset * 2);  const center = {    x: canvasWidth / 2,    y: canvasHeight / 2,  };  const maximumRadius = Math.min(width / 2, height / 2);  const radius = Math.max(    0,    Math.min(borderRadius - inset, maximumRadius),  );  const rotation = useDerivedValue(() => {    if (paused || reducedMotion) {      return 0;    }    return ((clock.get() % duration) / duration) * Math.PI * 2;  });  const transform = useDerivedValue(() => [    { rotate: rotation.get() },  ]);  const glowOpacity = Math.min(1, Math.max(0, intensity));  return {    center,    glowOpacity,    height,    inset,    radius,    transform,    width,  };}

API reference

Generated directly from the exported component props.

AuroraBorder