ThemeProvider and feedback
ThemeProvider hands every component its theme and an optional feedback handler, where your app plays its own haptics and sounds.
Usage
Mount one ThemeProvider at the root of your app and pass it the theme for the current appearance:
import { ThemeProvider, defaultDark, defaultLight, type FeedbackHandler } from "@terratcg/ui";
import * as Haptics from "expo-haptics";
import { useColorScheme } from "react-native";
const feedback: FeedbackHandler = (intent) => {
if (intent === "press") void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
else void Haptics.selectionAsync();
};
export function Root({ children }: { children: React.ReactNode }) {
const dark = useColorScheme() === "dark";
return (
<ThemeProvider theme={dark ? defaultDark : defaultLight} feedback={feedback}>
{children}
</ThemeProvider>
);
}Keep feedback stable, defined outside the component or in useCallback, so the theme context doesn't change on every render.
Props
Prop
Type
The theme
- One theme at a time:
ThemeProvidertakes a singletheme. Your app decides between light and dark, fromuseColorScheme()or a stored preference it owns. There is no light and dark pair prop. - Fallback: a component rendered with no
ThemeProviderabove it usesdefaultLight. - Nesting: an inner
ThemeProvideroverrides the theme for everything inside it. Pass itsfeedbackagain if that part of the screen should keep it. - Reading it:
useTheme()returns the activeTheme, for your own components that should match:useTheme().colors.ink,useTheme().spacing.md.
See Theming for every token and how to build your own theme.
Labels
TerraUI ships no translations. A few controls speak a word when you give them none of their own, such as a Spinner's accessibility label. Pass those words in your app's language through labels:
<ThemeProvider theme={theme} labels={{ loading: t("loading") }}>- Default: English ("Loading") when no provider supplies a label.
- Nesting: an inner
ThemeProviderthat leaves a label out keeps the surrounding one, so a theme switch on part of a screen need not repeat them. - Reading them:
useUiLabels()returns the activeUiLabels, for your own controls that should say the same words.
Feedback
TerraUI plays no haptic and no sound. When a user deliberately acts on a control, the control calls your feedback handler with an intent, and your app decides what to play.
| Intent | Reported by |
|---|---|
"press" | A Button (unless selected is set), IconButton, Shutter, the tab bar's central action, a pressable Cell, a pressable Row. |
"select" | A Button with selected set, Switch, SwitchRow, SelectableCell, a tab bar item. |
- Deliberate only: a control reports on activation, never while disabled or loading.
- Never blocking: without a handler, nothing happens and the action still runs. A missing haptics module must not stop a press.
- Outcomes are yours: feedback is the intent of a press, not the result of an operation. Report success and failure from your own code, for example with a toast.
- In overlays:
OverlayHostpasses the surrounding handler to the keys of its Dialogs and Menus.
useFeedback() returns the handler, so your own controls can report the same intents.
Rules
- Mount one
ThemeProviderat the root, around your navigator and yourOverlayHost. - Let your app own the appearance and any stored preference; pass
ThemeProviderthe resulting theme. - Keep haptics and sounds in your app, in the
feedbackhandler. Don't call them fromonPressas well, or each press plays twice. - Respect the user's settings in your handler, such as a sound setting or the silent switch.