TerraUI v0.1.1

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: ThemeProvider takes a single theme. Your app decides between light and dark, from useColorScheme() or a stored preference it owns. There is no light and dark pair prop.
  • Fallback: a component rendered with no ThemeProvider above it uses defaultLight.
  • Nesting: an inner ThemeProvider overrides the theme for everything inside it. Pass its feedback again if that part of the screen should keep it.
  • Reading it: useTheme() returns the active Theme, 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 ThemeProvider that 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 active UiLabels, 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.

IntentReported 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: OverlayHost passes 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 ThemeProvider at the root, around your navigator and your OverlayHost.
  • Let your app own the appearance and any stored preference; pass ThemeProvider the resulting theme.
  • Keep haptics and sounds in your app, in the feedback handler. Don't call them from onPress as well, or each press plays twice.
  • Respect the user's settings in your handler, such as a sound setting or the silent switch.

On this page