Expokit
AboutDocsComponentsChartsBlocksPricing
Sign inGet started
All components

Inputs

CalendarNewDate PickerNewFloating InputOTP InputPassword InputSearch BarSelectSliderStepperTag InputTextareaTime PickerNew

Selection

CheckboxChip GroupColor SwatchProRadio GroupRatingProSegmented ControlSwitch

Buttons

ButtonFloating Action ButtonProLike ButtonProMorph ButtonProShimmer ButtonProSlide to ConfirmProStateful Button

Feedback

AlertProgressSkeletonSpinnerToast

Overlays

Action SheetNewBottom SheetDialogDrawerNew

Display

AccordionAvatarBadgeCardCarouselProList ItemNewPagination DotsNewSwipeable RowProTimelineNew

Navigation

Bottom NavNewProgress StepsNewTabs

Motion

Number TickerProText Animation
Button
Sponsor ExpoKit

Free stays free. Tip once — keep shipping.

Support
About
Docs
IntroductionWhat ExpoKit is and how it worksInstallationAdd ExpoKit to an Expo appThemingColours, radius and dark modeCLIinit, add, login and updateAI assistants (MCP)Cursor, Claude, Antigravity and moreRun on your phoneTry components in Expo GoChangelogWhat shipped recently
npx @expokit/cli@latest initQuick start
Components
Liquid Glass8 iOS 26 glass pieces3DNew14 tilt, flip and stack
Inputs 12
  • CalendarNew
  • Date PickerNew
  • Floating Input
  • OTP Input
  • Password Input
  • +7 more
Selection 7
  • Checkbox
  • Chip Group
  • Color SwatchPro
  • Radio Group
  • RatingPro
  • +2 more
Buttons 7
  • Button
  • Floating Action ButtonPro
  • Like ButtonPro
  • Morph ButtonPro
  • Shimmer ButtonPro
  • +2 more
Feedback 5
  • Alert
  • Progress
  • Skeleton
  • Spinner
  • Toast
Overlays 4
  • Action SheetNew
  • Bottom Sheet
  • Dialog
  • DrawerNew
Display 9
  • Accordion
  • Avatar
  • Badge
  • Card
  • CarouselPro
  • +4 more
Navigation 3
  • Bottom NavNew
  • Progress StepsNew
  • Tabs
Motion 2
  • Number TickerPro
  • Text Animation
70 components · 347 live variantsAll components
Charts

Cartesian

  • Area Chart
  • Bar Chart
  • Line Chart
  • Combo Chart

Circular

  • Pie & Donut
  • Radial Chart
  • Radar Chart
  • Polar AreaPro

3D & Pixel

  • Isometric BarsPro
  • Pixel BarsPro

Specialty

  • TreemapPro
  • WaterfallPro
  • BulletPro
  • TimelinePro
  • Gauge
  • Candlestick
  • Heatmap
  • Funnel

Classic

  • Sparkline
  • Progress Rings
  • MultiLine (classic)
  • StackedBar (classic)
  • Ranking Bars (classic)

Guides

  • Usage & config
  • Patterns & effects
  • Show / hide anything
  • Chart theming
  • Interactions
23 charts, built on the open-source expo-charts engineAll charts
Blocks

Screens

ChatProAn AI assistant chat screenCheckoutProA premium checkoutDashboardProA finance / analytics home screenInboxProA messaging inboxOnboardingProA minimal swipeable welcome flowProfileProAccount profile for any appSearch ResultsProA clean, monochrome search screenSettingsProAn iOS-style grouped settings screenSign InProOne configurable sign-in flow modelled on top mobile apps
9 complete screens — drop one in, wire your dataSee them on devices All blocks
Pricing
DocsComponentsButtonsButton

Button

Scan with Expo Go

Button with all its variants, native in Expo Go. No account needed.

No Expo Go? Open in your browser

A button that springs down on press, in eleven variants including gradient and glow.

<Button />6 variantsiOS · Android · Expo Go
9:41

Sunday 18 October

9:41

Installation#

One command copies the source into your app — you own the code and can change anything.

Usage#

Colours, radius and light / dark come from useKit(). Wrap your app in <KitProvider accent="#6366f1" radius={14}> to re-skin every component at once.

Props#

PropTypeDefault
variant

Solid primary / accent / secondary / danger fill, bordered outline, transparent ghost, tinted soft, underlined link, two-tone gradient, accent with a coloured glow, or a raised elevated surface.

"primary" | "accent" | "secondary" | "outline" | "ghost" | "soft" | "destructive" | "link" | "gradient" | "glow" | "elevated""primary"
size

36 / 46 / 54pt, or a round icon button.

"sm" | "md" | "lg" | "icon""md"
loading

Spinner + blocks presses.

booleanfalse
left / right

Icons beside the label.

ReactNode—
block

Stretches to the parent's width (alignSelf: stretch).

booleanfalse
pill

Fully rounded.

booleanfalse
color

Any colour; recolours every tinted variant and picks readable text automatically.

stringtheme accent
gradientTo

Second stop for variant="gradient".

stringcolor shifted 38° on the hue wheel
haptics

Light tap on press.

booleanfalse
textStyle

Style for the label text (merged over size and variant typography).

StyleProp<TextStyle>—
style

Style for the outer animated wrapper (margins, alignment); the pressable surface keeps its own.

StyleProp<ViewStyle>—
…PressableProps

onPress, disabled, …

Behaviour & accessibility#

Touch
Scales down with a spring on press-in and bounces back on release. loading swaps the label for a spinner and blocks presses.
Haptics
Opt-in light impact on press with haptics; off by default.
Screen readers
A button with disabled and busy state.

Source#

PreviousSwitchNextFloating Action Button

Did you like the content?

Coding agents

Chat apps

Other

9:41

Variants

Button

Manual installation
  1. Install dependencies

    terminal
    npx expo install react-native-svg expo-haptics
  2. Copy the files

    Create each file at the same path and paste its code. @/ points at your src/ folder (the default in new Expo apps). Skip any file you already have, like lib/kit.tsx.

    • src/components/ui/button.tsx275 lines
    • src/lib/kit.tsx220 lines

    Want to read it first? Every file is in Source below.

  3. Use it

    App.tsx
    import { Button } from "@/components/ui/button";

    See Usage for a full example.

Example.tsx
import { Button } from '@/components/ui/button'; <Button variant="accent" size="lg" block onPress={checkout}>  Continue to payment</Button>

Every variant above has a complete, runnable example in its Code tab.

src/components/ui/button.tsx
import * as Haptics from 'expo-haptics';import React, { useId, useRef } from 'react';import {  ActivityIndicator,  Animated,  Pressable,  StyleSheet,  Text,  View,  type PressableProps,  type StyleProp,  type TextStyle,  type ViewStyle,} from 'react-native';import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg'; import { alpha, isTextNode, motion, readableOn, useKit } from '@/lib/kit'; export type ButtonVariant =  | 'primary'  | 'accent'  | 'secondary'  | 'outline'  | 'ghost'  | 'soft'  | 'destructive'  | 'link'  | 'gradient'  | 'glow'  | 'elevated';export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'> {  children?: React.ReactNode;  variant?: ButtonVariant;  size?: ButtonSize;  /** Swaps the label for a spinner and blocks presses. */  loading?: boolean;  left?: React.ReactNode;  right?: React.ReactNode;  /** Stretch to the parent's width. */  block?: boolean;  /** Fully rounded ends. */  pill?: boolean;  /**   * Any colour. Recolours every tinted variant (accent, soft, outline, ghost, link, gradient,   * glow, elevated); text on filled buttons picks black or white automatically.   */  color?: string;  /** Second gradient stop for `variant="gradient"` (default: `color` shifted along the hue wheel). */  gradientTo?: string;  /** Light tap on press (default false — most buttons shouldn't buzz). */  haptics?: boolean;  style?: StyleProp<ViewStyle>;  textStyle?: StyleProp<TextStyle>;} const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {  sm: { h: 36, px: 14, font: 14, gap: 6 },  md: { h: 46, px: 18, font: 15.5, gap: 8 },  lg: { h: 54, px: 22, font: 17, gap: 10 },  icon: { h: 44, px: 0, font: 15, gap: 0 },}; /** Rotates a #rrggbb colour around the hue wheel by `deg` and nudges its lightness by `dl` (0–1). */function shift(hex: string, deg: number, dl = 0) {  const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;  const n = parseInt(full.slice(1, 7), 16);  if (!hex.startsWith('#') || Number.isNaN(n)) return hex;  const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => v / 255);  const max = Math.max(r, g, b);  const min = Math.min(r, g, b);  const l = (max + min) / 2;  const d = max - min;  const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));  let h = 0;  if (d) h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;  h = (h * 60 + deg + 360) % 360;  const L = Math.min(1, Math.max(0, l + dl));  const c = (1 - Math.abs(2 * L - 1)) * s;  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));  const m = L - c / 2;  const [r1, g1, b1] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];  return '#' + [r1, g1, b1].map((v) => Math.round((v + m) * 255).toString(16).padStart(2, '0')).join('');} /** Midpoint of two #rrggbb colours (to pick readable text for a gradient). */function mid(a: string, b: string) {  const n = (h: string) => parseInt((h.length === 4 ? '#' + [...h.slice(1)].map((c) => c + c).join('') : h).slice(1, 7), 16);  const [x, y] = [n(a), n(b)];  if (!a.startsWith('#') || !b.startsWith('#') || Number.isNaN(x) || Number.isNaN(y)) return a;  return '#' + [16, 8, 0].map((sh) => Math.round((((x >> sh) & 255) + ((y >> sh) & 255)) / 2).toString(16).padStart(2, '0')).join('');} /** * Pressable with spring feedback, 11 variants, 4 sizes, icons and a loading state. Pass any * `color` and every tinted variant — including the gradient and glow looks — follows it. */export function Button({  children,  variant = 'primary',  size = 'md',  loading = false,  left,  right,  block,  pill,  color,  gradientTo,  haptics = false,  disabled,  style,  textStyle,  onPress,  onPressIn,  onPressOut,  ...props}: ButtonProps) {  const { colors, radius, dark } = useKit();  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');  const scale = useRef(new Animated.Value(1)).current;  const isDisabled = disabled || loading;  const tint = color ?? colors.accent;  const onTint = color ? readableOn(tint) : colors.accentForeground;  const s = SIZES[size];  const corner = pill || size === 'icon' ? radius.full : radius.md;  // Only the opt-in gradient variant gets a second tone; everything else is one solid colour.  const to = variant === 'gradient' ? gradientTo ?? shift(tint, 38, 0.04) : tint;   const look: Record<ButtonVariant, { bg: string; fg: string; border?: string }> = {    primary: { bg: colors.primary, fg: colors.primaryForeground },    accent: { bg: tint, fg: onTint },    secondary: { bg: colors.secondary, fg: colors.secondaryForeground },    outline: { bg: 'transparent', fg: color ?? colors.foreground, border: color ? alpha(tint, 0.5) : colors.border },    ghost: { bg: 'transparent', fg: color ?? colors.foreground },    soft: { bg: alpha(tint, dark ? 0.2 : 0.14), fg: tint },    destructive: { bg: colors.danger, fg: colors.dangerForeground },    link: { bg: 'transparent', fg: tint },    gradient: { bg: tint, fg: readableOn(mid(tint, to)) },    glow: { bg: tint, fg: onTint },    elevated: { bg: dark ? colors.popover : colors.card, fg: color ?? colors.foreground, border: alpha(colors.foreground, dark ? 0.1 : 0.06) },  };  const { bg, fg, border } = look[variant];  const solid = bg !== 'transparent';  const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';   const spring = (to: number) => Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();   const label = isTextNode(children) ? (    <Text      numberOfLines={1}      style={[styles.label, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}    >      {children}    </Text>  ) : (    children  );   return (    <Animated.View      style={[        { transform: [{ scale }] },        block && styles.block,        style,      ]}    >      <Pressable        accessibilityRole="button"        accessibilityState={{ disabled: !!isDisabled, busy: loading }}        disabled={isDisabled}        onPress={(e) => {          if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});          onPress?.(e);        }}        onPressIn={(e) => {          spring(0.96);          onPressIn?.(e);        }}        onPressOut={(e) => {          spring(1);          onPressOut?.(e);        }}        style={({ pressed }) => [          styles.base,          {            height: s.h,            width: size === 'icon' ? s.h : undefined,            paddingHorizontal: variant === 'link' ? 0 : s.px,            gap: s.gap,            borderRadius: corner,            backgroundColor: bg,            borderColor: border ?? 'transparent',            borderWidth: border ? StyleSheet.hairlineWidth * 2 : 0,            opacity: isDisabled ? 0.5 : pressed && variant === 'link' ? 0.6 : 1,          },          pressed && quiet && variant !== 'link' && { backgroundColor: color ? alpha(tint, 0.1) : colors.muted },          // Coloured glow under the button (iOS / web; Android shows plain elevation).          variant === 'glow' && !isDisabled && [styles.glow, { shadowColor: tint }],          variant === 'elevated' && styles.elevated,        ]}        {...props}      >        {({ pressed }) => (          <>            {variant === 'gradient' || variant === 'glow' ? (              // Two-tone fill (gradient) or a faint white top sheen on the solid glow fill, drawn behind the content.              <View pointerEvents="none" style={[StyleSheet.absoluteFill, { borderRadius: corner, overflow: 'hidden' }]}>                <Svg width="100%" height="100%">                  <Defs>                    <LinearGradient id={gid} x1="0" y1="0" x2="1" y2="1">                      {variant === 'gradient'                        ? [                            <Stop key="a" offset="0" stopColor={tint} />,                            <Stop key="b" offset="1" stopColor={to} />,                          ]                        : [                            <Stop key="a" offset="0" stopColor="#ffffff" stopOpacity={0.12} />,                            <Stop key="b" offset="0.55" stopColor="#ffffff" stopOpacity={0} />,                          ]}                    </LinearGradient>                  </Defs>                  <Rect width="100%" height="100%" fill={`url(#${gid})`} />                </Svg>              </View>            ) : null}            {/* Press feedback on filled buttons: a light wash on dark fills, a shade on light ones. */}            {solid && pressed && !quiet ? (              <View                pointerEvents="none"                style={[StyleSheet.absoluteFill, { borderRadius: corner, backgroundColor: readableOn(bg) === '#ffffff' ? alpha('#ffffff', 0.14) : alpha('#000000', 0.08) }]}              />            ) : null}            {loading ? (              <View style={styles.row}>                <ActivityIndicator size="small" color={fg} />                {size !== 'icon' && isTextNode(children) ? (                  <Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>                ) : null}              </View>            ) : (              <>                {left}                {label}                {right}              </>            )}          </>        )}      </Pressable>    </Animated.View>  );} const styles = StyleSheet.create({  base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },  block: { alignSelf: 'stretch' },  label: { fontWeight: '400', letterSpacing: -0.1 },  link: { textDecorationLine: 'underline' },  glow: {    shadowOpacity: 0.45,    shadowRadius: 14,    shadowOffset: { width: 0, height: 6 },    elevation: 6,  },  elevated: {    shadowColor: '#000',    shadowOpacity: 0.1,    shadowRadius: 10,    shadowOffset: { width: 0, height: 3 },    elevation: 3,  },});