Feedback
A dialog for writing — "What should we fix first?", a bug report, a one-question survey — with the field in a recessed well and the actions on the band around it.
Installation
Feedback ships with the library — install it once, then import:
import { Feedback } from "@delacour/react-native-ui/feedback";Or copy the source into your project, to own and edit it:
bunx delacour@latest add feedbackFeedback is a Dialog, so it draws through the
overlay foundation: mount OverlayProvider once at your app's
root, inside DelacourProvider.
Usage
import { Button } from "@/components/ui/button";
import { Feedback, useFeedback } from "@/components/ui/feedback";
import { View } from "react-native";
function Send() {
const { clear, close } = useFeedback();
return (
<Feedback.Submit
onSubmit={async (message) => {
await sendFeedback(message);
close();
clear();
}}
/>
);
}
export default function Example() {
return (
<View className="flex-1 items-center justify-center p-4">
<Feedback>
<Feedback.Trigger asChild>
<Button variant="secondary">Give feedback</Button>
</Feedback.Trigger>
<Feedback.Content>
<Feedback.Panel>
<Feedback.Title>What should we fix first?</Feedback.Title>
<Feedback.Close />
<Feedback.Field placeholder="Tell us what got in your way" />
</Feedback.Panel>
<Feedback.Footer>
<Feedback.Cancel />
<Send />
</Feedback.Footer>
</Feedback.Content>
</Feedback>
</View>
);
}Anatomy
<Feedback>
<Feedback.Trigger />
<Feedback.Content>
<Feedback.Panel>
<Feedback.Title />
<Feedback.Close />
<Feedback.Field />
</Feedback.Panel>
<Feedback.Footer>
<Feedback.Action />
<Feedback.Cancel />
<Feedback.Submit />
</Feedback.Footer>
</Feedback.Content>
</Feedback>Feedback.Content is the shell — a dialog card on the muted band colour. Feedback.Panel is the
recessed well inside it, on the page colour, which is what tells the user where to type.
Feedback.Field has no border of its own; the well is its box. The footer's actions sit on the band
under the well.
Use Feedback for writing. For a confirmation or a warning, use a Dialog.
The draft
The draft belongs to the root, not to the field, so it survives the dialog closing and opening
again: Cancel, the ✕, the scrim and Android back all keep it. useFeedback().clear() is the one
way to empty it — call it once a send has gone through.
Pass value and onValueChange to own the draft — to prefill it, or read it while the dialog is
closed:
const [draft, setDraft] = useState("");
<Feedback value={draft} onValueChange={setDraft}>…</Feedback>;Sending
Feedback.Submit is disabled while the draft is empty or only whitespace, and passes onSubmit
the trimmed text. It never closes the dialog. When onSubmit returns a promise, the button shows
its spinner and the field is read-only until the promise settles — and the dialog is still open, so
a failure can be shown in the well with the draft intact. Close and clear from onSubmit on
success.
function Send() {
const { clear, close } = useFeedback();
const [error, setError] = useState<string | null>(null);
return (
<>
{error ? <Text.Caption color="destructive">{error}</Text.Caption> : null}
<Feedback.Submit
onSubmit={async (message) => {
try {
await sendFeedback(message);
close();
clear();
} catch {
setError("That didn't send. Try again.");
}
}}
/>
</>
);
}Handle failures inside onSubmit. A rejected promise is not caught for you, so it still reaches your
app's error reporting rather than vanishing.
Chips and empty messages
canSubmitEmpty lets Submit through with no text — for when chips or a rating carry the answer.
Tie it to whatever the user picked:
<Feedback.Panel>
<Feedback.Title>What got in your way?</Feedback.Title>
<View className="flex-row flex-wrap gap-2">{chips}</View>
<Feedback.Field minRows={3} placeholder="Anything else?" />
</Feedback.Panel>
<Feedback.Footer>
<Feedback.Cancel />
<Feedback.Submit canSubmitEmpty={picked.size > 0} onSubmit={send} />
</Feedback.Footer>Multiple steps
A rating, then a message, then thanks: swap the panel's and the footer's children as the user moves
on. The well eases to each step's height over 200ms and the card grows or shrinks with it, staying
centred, so nothing jumps. Feedback.Action is a secondary button for the steps' own actions —
Next, Back, Done.
<Feedback.Content>
{step === "rating" ? (
<>
<Feedback.Panel>
<Feedback.Title>How was checkout?</Feedback.Title>
<Rating onChange={setScore} value={score}>
<Rating.Stars />
</Rating>
</Feedback.Panel>
<Feedback.Footer>
<Feedback.Cancel />
<Feedback.Action isDisabled={score === 0} onPress={() => setStep("message")} variant="primary">
Next
</Feedback.Action>
</Feedback.Footer>
</>
) : (
…
)}
</Feedback.Content>Keyboard
The dialog lifts just far enough to keep its footer above the keyboard while the field has focus,
and settles back when the keyboard closes. The field starts six rows tall (minRows), grows with
the text, and scrolls past twelve (maxRows), so a long message never pushes Send under the
keyboard.
Accessibility
Everything a Dialog does: the card is modal to
assistive technology, labelled by the title, and focus moves to the title on open and back to the
trigger on close. When the title is plain text it is also the field's label, so a screen reader
announces the question rather than only the placeholder. The ✕ is labelled Close. Submit
announces its busy state while a send is in flight.
Under Reduce Motion the card fades without growing, and the well changes height without easing.
API Reference
Feedback
Prop
Type
Feedback.Trigger
Dialog.Trigger. With asChild it hands its press to the single child.
Feedback.Content
Dialog.Content — size, className, scrimClassName and React Native's ViewProps.
Feedback.Panel, Feedback.Footer
Take React Native's ViewProps and className.
Feedback.Title
Dialog.Title — Text's props.
Feedback.Close
Dialog.Close — the ✕ in the well's corner, or with asChild any control that closes.
Feedback.Field
Takes TextInput's props, less value, onChangeText and multiline — the draft is the root's.
Prop
Type
Feedback.Action, Feedback.Cancel
Take Button's props. Both are secondary by default. Cancel is labelled "Cancel" and closes the
dialog, keeping the draft.
Feedback.Submit
Takes Button's props, less onPress. Labelled "Send" unless given children.
Prop
Type
useFeedback
Reads the enclosing feedback dialog. Throws outside a Feedback.
Prop
Type