v0.1 public beta early access on GitHub
Home / Screens / core-feedback
Screen template

Send feedback Screen

Topic chips, message composer, and submit flow with confirmation.

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add core-feedback
import { FeedbackScreen } from "@/screens/core/feedback";
Registry dependencies6
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
core/feedback.tsxsource · yours to edit
import * as React from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, Glass, Icon, Input, Text, Textarea, } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, StickyBottomBar } from "../shared";

export interface FeedbackScreenProps {
  topics?: string[];
  onSubmit?: (topic: string, message: string) => void;
  onBack?: () => void;
}

const DEFAULT_TOPICS = [
  "Bug report",
  "Feature request",
  "Design feedback",
  "Account issue",
  "Something else",
];

/** Feedback — topic chips, message composer, submit CTA. */
export function FeedbackScreen({
  topics = DEFAULT_TOPICS,
  onSubmit,
  onBack,
}: FeedbackScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [topic, setTopic] = React.useState(topics[0] ?? "Bug report");
  const [message, setMessage] = React.useState(
    "The chip group on the search screen overflows on a 320px viewport — the last chip wraps under the field instead of scrolling."
  );
  const [email, setEmail] = React.useState("amara.chen@lumio.dev");
  const [score, setScore] = React.useState<number | null>(7);
  const [sent, setSent] = React.useState(false);

  const submit = () => {
    haptics.medium();
    setSent(true);
    onSubmit?.(topic, message);
  };

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back pill + hug-label title pill. */}
      <View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
        <View className="flex-row items-center gap-2">
          <Glass
            effect="regular"
            className="h-11 w-11 items-center justify-center rounded-full"
          >
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityLabel="Go back"
              className="h-9 w-9"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Button>
          </Glass>
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <Glass
            effect="regular"
            className="h-11 flex-row items-center justify-center rounded-full px-5"
          >
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Send feedback
            </Text>
          </Glass>
        </View>
      </View>

      {sent ? (
        <Mount className="flex-1 items-center justify-center gap-3 px-8">
          <View className="h-16 w-16 items-center justify-center rounded-full bg-success">
            <Icon name="check" size="md" color="successForeground" />
          </View>
          <Text variant="h3" className="pt-2 text-center">
            Thanks for the feedback
          </Text>
          <Text variant="small" className="text-center text-muted-foreground">
            We read every message — expect a reply within two business days.
          </Text>
        </Mount>
      ) : (
        <>
          <ScrollView
            showsVerticalScrollIndicator={false}
            keyboardShouldPersistTaps="handled"
            contentContainerStyle={{
              paddingTop: insets.top + 60,
              paddingBottom: insets.bottom + 120,
              paddingHorizontal: 24,
              gap: 24,
            }}
          >
            <View className="gap-3 pt-4">
              <Text variant="small" className="text-muted-foreground">
                What is this about?
              </Text>
              <View className="flex-row flex-wrap gap-2">
                {topics.map((t) => (
                  <Chip
                    key={t}
                    variant={topic === t ? "assist" : "filter"}
                    selected={topic === t}
                    selectedClassName="border-[#BEF04C] bg-[#BEF04C]"
                    selectedTextClassName="font-semibold text-[#1B1B26]"
                    onPress={() => {
                      haptics.selection();
                      setTopic(t);
                    }}
                    accessibilityState={{ selected: topic === t }}
                  >
                    {t}
                  </Chip>
                ))}
              </View>
            </View>

            {/* Satisfaction scale — Jupiter's 0–10 idiom: pick a number,
                labels anchor the ends. */}
            <View className="gap-3">
              <Text variant="small" className="text-muted-foreground">
                Overall, how's it going?
              </Text>
              <View className="flex-row justify-between">
                {Array.from({ length: 11 }, (_, n) => {
                  const active = score === n;
                  return (
                    <Press
                      key={n}
                      haptic="light"
                      pressedScale={0.9}
                      onPress={() => setScore(n)}
                      accessibilityRole="radio"
                      accessibilityState={{ selected: active }}
                      accessibilityLabel={`Rate ${n} out of 10`}
                      className={cn(
                        "h-9 w-9 items-center justify-center overflow-hidden rounded-full border",
                        active ? "border-[#BEF04C] bg-[#BEF04C]" : "border-transparent"
                      )}
                    >
                      {!active ? (
                        <View className="absolute inset-0 rounded-full bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
                      ) : null}
                      <Text
                        className={cn(
                          "text-sm tabular-nums",
                          active ? "font-semibold text-[#1B1B26]" : "text-muted-foreground"
                        )}
                      >
                        {n}
                      </Text>
                    </Press>
                  );
                })}
              </View>
              <View className="flex-row justify-between">
                <Text variant="small" className="text-xs text-muted-foreground">
                  Rough
                </Text>
                <Text variant="small" className="text-xs text-muted-foreground">
                  Loving it
                </Text>
              </View>
            </View>

            <View className="gap-2">
              <Text variant="small" className="text-muted-foreground">
                Tell us more
              </Text>
              <Textarea
                value={message}
                onChangeText={setMessage}
                placeholder="Describe what happened, or what you'd like to see…"
                minHeight={120}
                accessibilityLabel="Feedback message"
              />
              {/* Screenshot affordance — bug reports almost always want one. */}
              <Press
                haptic="light"
                accessibilityRole="button"
                accessibilityLabel="Attach a screenshot"
                className="min-h-11 flex-row items-center gap-2.5 rounded-2xl bg-white px-4 dark:bg-[#1A1A20]"
              >
                <Icon name="camera" size="sm" color="mutedForeground" />
                <Text variant="small" className="text-muted-foreground">
                  Attach a screenshot — helps us see what you see
                </Text>
              </Press>
            </View>

            <View className="gap-2">
              <Text variant="small" className="text-muted-foreground">
                Email (optional)
              </Text>
              <Input
                value={email}
                onChangeText={setEmail}
                placeholder="you@example.com"
                keyboardType="email-address"
                autoCapitalize="none"
                accessibilityLabel="Contact email"
              />
              <Text variant="small" className="text-muted-foreground">
                We'll only use this to follow up on your report.
              </Text>
            </View>
          </ScrollView>

          <StickyBottomBar>
            <Button
              className="w-full bg-[#BEF04C]"
              textClassName="text-[#1B1B26]"
              haptic="medium"
              onPress={submit}
              disabled={message.trim().length < 10}
              accessibilityLabel="Send feedback"
            >
              Send feedback
            </Button>
          </StickyBottomBar>
        </>
      )}
    </SafeAreaView>
  );
}
FeedbackScreen.displayName = "FeedbackScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Send feedback Screen" screen in React Native using LumioUI.

Goal: Topic chips, message composer, and submit flow with confirmation. Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add core-feedback
```

Or pull just the components: `npx lumioui add button chip icon input text textarea`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×2

### `chip` — Compact selectable pill with press scale and ink overlay.
- `<Chip>` ×1

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3

### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×12

### `textarea` — Multiline field with auto-grow and character count.
- `<Textarea>` ×1

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/core/feedback.tsx` exporting FeedbackScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, Glass, Icon, Input, Text, Textarea, } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, StickyBottomBar } from "../shared";

export interface FeedbackScreenProps {
  topics?: string[];
  onSubmit?: (topic: string, message: string) => void;
  onBack?: () => void;
}

const DEFAULT_TOPICS = [
  "Bug report",
  "Feature request",
  "Design feedback",
  "Account issue",
  "Something else",
];

/** Feedback — topic chips, message composer, submit CTA. */
export function FeedbackScreen({
  topics = DEFAULT_TOPICS,
  onSubmit,
  onBack,
}: FeedbackScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [topic, setTopic] = React.useState(topics[0] ?? "Bug report");
  const [message, setMessage] = React.useState(
    "The chip group on the search screen overflows on a 320px viewport — the last chip wraps under the field instead of scrolling."
  );
  const [email, setEmail] = React.useState("amara.chen@lumio.dev");
  const [score, setScore] = React.useState<number | null>(7);
  const [sent, setSent] = React.useState(false);

  const submit = () => {
    haptics.medium();
    setSent(true);
    onSubmit?.(topic, message);
  };

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back pill + hug-label title pill. */}
      <View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
        <View className="flex-row items-center gap-2">
          <Glass
            effect="regular"
            className="h-11 w-11 items-center justify-center rounded-full"
          >
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityLabel="Go back"
              className="h-9 w-9"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Button>
          </Glass>
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <Glass
            effect="regular"
            className="h-11 flex-row items-center justify-center rounded-full px-5"
          >
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Send feedback
            </Text>
          </Glass>
        </View>
      </View>

      {sent ? (
        <Mount className="flex-1 items-center justify-center gap-3 px-8">
          <View className="h-16 w-16 items-center justify-center rounded-full bg-success">
            <Icon name="check" size="md" color="successForeground" />
          </View>
          <Text variant="h3" className="pt-2 text-center">
            Thanks for the feedback
          </Text>
          <Text variant="small" className="text-center text-muted-foreground">
            We read every message — expect a reply within two business days.
          </Text>
        </Mount>
      ) : (
        <>
          <ScrollView
            showsVerticalScrollIndicator={false}
            keyboardShouldPersistTaps="handled"
            contentContainerStyle={{
              paddingTop: insets.top + 60,
              paddingBottom: insets.bottom + 120,
              paddingHorizontal: 24,
              gap: 24,
            }}
          >
            <View className="gap-3 pt-4">
              <Text variant="small" className="text-muted-foreground">
                What is this about?
              </Text>
              <View className="flex-row flex-wrap gap-2">
                {topics.map((t) => (
                  <Chip
                    key={t}
                    variant={topic === t ? "assist" : "filter"}
                    selected={topic === t}
                    selectedClassName="border-[#BEF04C] bg-[#BEF04C]"
                    selectedTextClassName="font-semibold text-[#1B1B26]"
                    onPress={() => {
                      haptics.selection();
                      setTopic(t);
                    }}
                    accessibilityState={{ selected: topic === t }}
                  >
                    {t}
                  </Chip>
                ))}
              </View>
            </View>

            {/* Satisfaction scale — Jupiter's 0–10 idiom: pick a number,
                labels anchor the ends. */}
            <View className="gap-3">
              <Text variant="small" className="text-muted-foreground">
                Overall, how's it going?
              </Text>
              <View className="flex-row justify-between">
                {Array.from({ length: 11 }, (_, n) => {
                  const active = score === n;
                  return (
                    <Press
                      key={n}
                      haptic="light"
                      pressedScale={0.9}
                      onPress={() => setScore(n)}
                      accessibilityRole="radio"
                      accessibilityState={{ selected: active }}
                      accessibilityLabel={`Rate ${n} out of 10`}
                      className={cn(
                        "h-9 w-9 items-center justify-center overflow-hidden rounded-full border",
                        active ? "border-[#BEF04C] bg-[#BEF04C]" : "border-transparent"
                      )}
                    >
                      {!active ? (
                        <View className="absolute inset-0 rounded-full bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
                      ) : null}
                      <Text
                        className={cn(
                          "text-sm tabular-nums",
                          active ? "font-semibold text-[#1B1B26]" : "text-muted-foreground"
                        )}
                      >
                        {n}
                      </Text>
                    </Press>
                  );
                })}
              </View>
              <View className="flex-row justify-between">
                <Text variant="small" className="text-xs text-muted-foreground">
                  Rough
                </Text>
                <Text variant="small" className="text-xs text-muted-foreground">
                  Loving it
                </Text>
              </View>
            </View>

            <View className="gap-2">
              <Text variant="small" className="text-muted-foreground">
                Tell us more
              </Text>
              <Textarea
                value={message}
                onChangeText={setMessage}
                placeholder="Describe what happened, or what you'd like to see…"
                minHeight={120}
                accessibilityLabel="Feedback message"
              />
              {/* Screenshot affordance — bug reports almost always want one. */}
              <Press
                haptic="light"
                accessibilityRole="button"
                accessibilityLabel="Attach a screenshot"
                className="min-h-11 flex-row items-center gap-2.5 rounded-2xl bg-white px-4 dark:bg-[#1A1A20]"
              >
                <Icon name="camera" size="sm" color="mutedForeground" />
                <Text variant="small" className="text-muted-foreground">
                  Attach a screenshot — helps us see what you see
                </Text>
              </Press>
            </View>

            <View className="gap-2">
              <Text variant="small" className="text-muted-foreground">
                Email (optional)
              </Text>
              <Input
                value={email}
                onChangeText={setEmail}
                placeholder="you@example.com"
                keyboardType="email-address"
                autoCapitalize="none"
                accessibilityLabel="Contact email"
              />
              <Text variant="small" className="text-muted-foreground">
                We'll only use this to follow up on your report.
              </Text>
            </View>
          </ScrollView>

          <StickyBottomBar>
            <Button
              className="w-full bg-[#BEF04C]"
              textClassName="text-[#1B1B26]"
              haptic="medium"
              onPress={submit}
              disabled={message.trim().length < 10}
              accessibilityLabel="Send feedback"
            >
              Send feedback
            </Button>
          </StickyBottomBar>
        </>
      )}
    </SafeAreaView>
  );
}
FeedbackScreen.displayName = "FeedbackScreen";
```

Screen: https://lumioui.dev/screens/core-feedback
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗