diff --git a/docs/UNISTYLES.md b/docs/UNISTYLES.md
index bbb00093c..8ed81e891 100644
--- a/docs/UNISTYLES.md
+++ b/docs/UNISTYLES.md
@@ -31,7 +31,7 @@ const styles = StyleSheet.create((theme) => ({
On first mount this can paint with the current adaptive or initial theme. If app settings later load a persisted theme and call [`UnistylesRuntime.setTheme`](https://www.unistyl.es/v3/guides/theming#change-theme), the JS-side style proxy may report the new theme while the native content container keeps the old background. That is how the welcome screen ended up with a light background and dark foreground/buttons.
-This applies broadly to non-`style` props that carry theme-dependent values, such as component props named `color`, `trackColor`, `tintColor`, and library-specific style props. The [3rd-party view decision algorithm](https://www.unistyl.es/v3/references/3rd-party-views) recommends explicit handling for these cases, and [issue #1030](https://github.com/jpudysz/react-native-unistyles/issues/1030) shows a related native-prop update edge case around `Image.tintColor`. Treat these values as React props unless wrapped with `withUnistyles`.
+This applies broadly to non-`style` props that carry theme-dependent values, such as component props named `color`, `trackColor`, `tintColor`, `backgroundStyle`, `handleIndicatorStyle`, and other library-specific style props. The [3rd-party view decision algorithm](https://www.unistyl.es/v3/references/3rd-party-views) recommends explicit handling for these cases, and [issue #1030](https://github.com/jpudysz/react-native-unistyles/issues/1030) shows a related native-prop update edge case around `Image.tintColor`. Treat these values as React props unless wrapped with `withUnistyles`.
## Fix Patterns
@@ -105,6 +105,44 @@ const { theme } = useUnistyles();
Keep layout and typography in `StyleSheet.create`; move only the stale theme-dependent value through React. If a larger subtree shows the same behavior, consider remounting the sheet on theme changes or moving the themed paint onto a wrapper that is mounted with the visible content.
+The same rule applies to bottom-sheet component props such as `backgroundStyle` and `handleIndicatorStyle`: they are library props, not the direct React Native `style` prop Unistyles registers. Prefer a custom `backgroundComponent` that calls `useUnistyles()`, or pass a small inline object from the hook theme.
+
+## Memoized Style Objects
+
+When a third-party library receives a plain style object, it is outside Unistyles' native tracking path. Make sure any memo that builds that style object depends on the actual theme values it reads.
+
+Avoid indirect keys like this:
+
+```tsx
+const { theme, rt } = useUnistyles();
+const markdownStyles = useMemo(() => createMarkdownStyles(theme), [rt.themeName]);
+```
+
+On adaptive system-theme changes, the hook can provide a light/dark theme update while an indirect runtime key is not the value that invalidates the memo. That leaves the library rendering stale colors. Assistant markdown hit this exact failure: the workspace shell switched to light, but assistant text and code spans kept the old dark-theme markdown style object.
+
+Prefer the hook theme itself, or explicit theme tokens, as the dependency:
+
+```tsx
+const { theme } = useUnistyles();
+const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
+```
+
+If a style factory is cheap, skipping `useMemo` entirely is also fine.
+
+## Static Theme Imports
+
+Do not import `theme` from `@/styles/theme` for live UI colors. That export is a dark-theme compatibility default, so using it in render code leaves icons, placeholders, or third-party props pinned to dark colors in light mode.
+
+Use `useUnistyles()` inside the component instead:
+
+```tsx
+const { theme } = useUnistyles();
+
+
+```
+
+Importing `baseColors`, theme-name constants, or `type Theme` is fine when the value is intentionally static or type-only.
+
## Adaptive Themes And Persisted Settings
Unistyles [`initialTheme`](https://www.unistyl.es/v3/guides/theming#select-theme) and [`adaptiveThemes`](https://www.unistyl.es/v3/guides/theming#adaptive-themes) are mutually exclusive. `initialTheme` can be a string or a synchronous function, but it cannot wait on async storage.
diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx
index 47e39166c..d5e015d71 100644
--- a/packages/app/src/components/adaptive-modal-sheet.tsx
+++ b/packages/app/src/components/adaptive-modal-sheet.tsx
@@ -109,9 +109,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[4],
flexGrow: 1,
},
- bottomSheetHandle: {
- backgroundColor: theme.colors.surface2,
- },
bottomSheetHeader: {
paddingHorizontal: theme.spacing[6],
paddingTop: theme.spacing[4],
@@ -243,7 +240,7 @@ export function AdaptiveModalSheet({
enablePanDownToClose
stackBehavior={stackBehavior}
backgroundComponent={SheetBackground}
- handleIndicatorStyle={styles.bottomSheetHandle}
+ handleIndicatorStyle={{ backgroundColor: theme.colors.surface2 }}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
accessible={false}
diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
index d8a12afd3..231348280 100644
--- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
+++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
@@ -22,7 +22,6 @@ import {
ShieldAlert,
ShieldOff,
} from "lucide-react-native";
-import { theme as defaultTheme } from "@/styles/theme";
import type {
AgentMode,
AgentModelDefinition,
@@ -86,6 +85,8 @@ export function DropdownField({
renderTrigger,
testID,
}: DropdownFieldProps): ReactElement {
+ const { theme } = useUnistyles();
+
if (renderTrigger) {
return (
<>
@@ -115,7 +116,7 @@ export function DropdownField({
{value || placeholder}
-
+
{errorMessage ? {errorMessage} : null}
{warningMessage ? {warningMessage} : null}
@@ -151,6 +152,8 @@ export function SelectField({
valueEllipsizeMode,
testID,
}: SelectFieldProps): ReactElement {
+ const { theme } = useUnistyles();
+
const getWebKey = useCallback((event: unknown): string | null => {
if (!event || typeof event !== "object") return null;
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
@@ -210,7 +213,7 @@ export function SelectField({
{value || placeholder || "Select..."}
-
+
{errorMessage ? {errorMessage} : null}
{warningMessage ? {warningMessage} : null}
@@ -229,7 +232,21 @@ interface DropdownSheetProps {
}
function DropdownSheetBackground({ style }: BottomSheetBackgroundProps) {
- return ;
+ const { theme } = useUnistyles();
+
+ return (
+
+ );
}
export function DropdownSheet({
@@ -238,6 +255,7 @@ export function DropdownSheet({
onClose,
children,
}: DropdownSheetProps): ReactElement {
+ const { theme } = useUnistyles();
const bottomSheetRef = useRef(null);
const snapPoints = useMemo(() => ["60%", "90%"], []);
@@ -280,12 +298,12 @@ export function DropdownSheet({
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundComponent={DropdownSheetBackground}
- handleIndicatorStyle={styles.bottomSheetHandle}
+ handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
>
- {title}
+ {title}
-
+
{
if (!event || typeof event !== "object") return null;
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
@@ -469,7 +489,7 @@ export function FormSelectTrigger({
{showLabel ? {label} : null}
{isLoading ? (
-
+
) : (
)}
-
+
);
}
@@ -518,6 +538,8 @@ export function AgentConfigRow({
onSelectThinkingOption,
disabled,
}: AgentConfigRowProps): ReactElement {
+ const { theme } = useUnistyles();
+
const providerOptions: ComboSelectOption[] = useMemo(
() =>
providerDefinitions.map((def) => ({
@@ -576,7 +598,7 @@ export function AgentConfigRow({
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
disabled={disabled || providerOptions.length === 0}
onSelect={onSelectProvider}
- icon={}
+ icon={}
showLabel={false}
testID="draft-provider-select"
/>
@@ -591,9 +613,7 @@ export function AgentConfigRow({
disabled={disabled}
isLoading={isModelLoading}
onSelect={onSelectModel}
- icon={
-
- }
+ icon={}
showLabel={false}
testID="draft-model-select"
/>
@@ -607,7 +627,7 @@ export function AgentConfigRow({
placeholder="Default"
disabled={disabled || modeOptions.length === 0}
onSelect={onSelectMode}
- icon={}
+ icon={}
showLabel={false}
testID="draft-mode-select"
/>
@@ -622,9 +642,7 @@ export function AgentConfigRow({
placeholder="Select..."
disabled={disabled}
onSelect={onSelectThinkingOption}
- icon={
-
- }
+ icon={}
showLabel={false}
/>
@@ -969,6 +987,8 @@ export function GitOptionsSection({
attachWorktreeError,
onSelectWorktreePath,
}: GitOptionsSectionProps): ReactElement {
+ const { theme } = useUnistyles();
+
const isLoading = status === "loading";
const isCreateMode = worktreeMode === "create";
const isAttachMode = worktreeMode === "attach";
@@ -1103,7 +1123,7 @@ export function GitOptionsSection({
autoCapitalize="none"
autoCorrect={false}
placeholder="branch name"
- placeholderTextColor={defaultTheme.colors.foregroundMuted}
+ placeholderTextColor={theme.colors.foregroundMuted}
onSubmitEditing={handleConfirmEdit}
/>
-
+
-
+
) : (
{displayBranch}
-
+
)}
@@ -1171,14 +1188,6 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
},
- bottomSheetBackground: {
- backgroundColor: theme.colors.surface2,
- borderTopLeftRadius: theme.borderRadius["2xl"],
- borderTopRightRadius: theme.borderRadius["2xl"],
- },
- bottomSheetHandle: {
- backgroundColor: theme.colors.palette.zinc[600],
- },
bottomSheetHeader: {
flexDirection: "row",
alignItems: "center",
diff --git a/packages/app/src/components/composer.tsx b/packages/app/src/components/composer.tsx
index 24fe17f3f..cdac24034 100644
--- a/packages/app/src/components/composer.tsx
+++ b/packages/app/src/components/composer.tsx
@@ -33,7 +33,7 @@ import {
type MessageInputRef,
type AttachmentMenuItem,
} from "./message-input";
-import { Theme } from "@/styles/theme";
+import type { Theme } from "@/styles/theme";
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import { encodeImages } from "@/utils/encode-images";
import { focusWithRetries } from "@/utils/web-focus";
diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx
index 71c4531f8..f57f52bf5 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -53,7 +53,6 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
-import { theme } from "@/styles/theme";
import { createMarkdownStyles } from "@/styles/markdown-styles";
import { Colors, Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
@@ -341,6 +340,7 @@ export const UserMessage = memo(function UserMessage({
isLastInGroup = true,
disableOuterSpacing,
}: UserMessageProps) {
+ const { theme } = useUnistyles();
const isCompact = useIsCompactFormFactor();
const [messageHovered, setMessageHovered] = useState(false);
const [copyButtonHovered, setCopyButtonHovered] = useState(false);
@@ -962,10 +962,10 @@ export const AssistantMessage = memo(function AssistantMessage({
client,
disableOuterSpacing,
}: AssistantMessageProps) {
- const { theme, rt } = useUnistyles();
+ const { theme } = useUnistyles();
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
- const markdownStyles = useMemo(() => createMarkdownStyles(theme), [rt.themeName]);
+ const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
const markdownParser = useMemo(() => {
const parser = MarkdownIt({ typographer: true, linkify: true });
diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx
index ef1089f91..bd8f9fecc 100644
--- a/packages/app/src/components/terminal-pane.tsx
+++ b/packages/app/src/components/terminal-pane.tsx
@@ -86,7 +86,7 @@ export function TerminalPane({ serverId, cwd, terminalId, isPaneFocused }: Termi
const isScreenFocused = useIsFocused();
const isAppVisible = useAppVisible();
const { theme } = useUnistyles();
- const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]);
+ const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]);
const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView);
const openAgentList = usePanelStore((state) => state.openAgentList);
diff --git a/packages/app/src/components/tool-call-sheet.tsx b/packages/app/src/components/tool-call-sheet.tsx
index 03568b35e..617b82a64 100644
--- a/packages/app/src/components/tool-call-sheet.tsx
+++ b/packages/app/src/components/tool-call-sheet.tsx
@@ -59,6 +59,7 @@ interface ToolCallSheetProviderProps {
}
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
+ const { theme } = useUnistyles();
const bottomSheetRef = useRef(null);
const [sheetData, setSheetData] = React.useState(null);
@@ -103,8 +104,8 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
onChange={handleSheetChange}
backdropComponent={renderBackdrop}
enablePanDownToClose
- backgroundStyle={styles.sheetBackground}
- handleIndicatorStyle={styles.handleIndicator}
+ backgroundComponent={CustomSheetBackground}
+ handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
>
{sheetData && }
@@ -120,6 +121,7 @@ interface ToolCallSheetContentProps {
}
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
+ const { theme } = useUnistyles();
const { toolName, displayName, detail, errorText, showLoadingSkeleton } = data;
const IconComponent = resolveToolCallIcon(toolName, detail);
@@ -129,13 +131,13 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
{/* Header */}
-
+
{displayName}
-
+
@@ -155,12 +157,6 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
// ----- Styles -----
const styles = StyleSheet.create((theme) => ({
- sheetBackground: {
- backgroundColor: theme.colors.surface2,
- },
- handleIndicator: {
- backgroundColor: theme.colors.palette.zinc[600],
- },
container: {
flex: 1,
backgroundColor: theme.colors.surface2,
@@ -180,9 +176,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2],
flex: 1,
},
- headerIcon: {
- color: theme.colors.foreground,
- },
headerTitle: {
fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.semibold,
@@ -192,9 +185,6 @@ const styles = StyleSheet.create((theme) => ({
closeButton: {
padding: theme.spacing[2],
},
- closeIcon: {
- color: theme.colors.foregroundMuted,
- },
content: {
flex: 1,
minHeight: 0,
diff --git a/packages/app/src/components/ui/autocomplete.tsx b/packages/app/src/components/ui/autocomplete.tsx
index 95ee62f29..49f920148 100644
--- a/packages/app/src/components/ui/autocomplete.tsx
+++ b/packages/app/src/components/ui/autocomplete.tsx
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from "react";
import { ScrollView, Text, View, Pressable, type LayoutChangeEvent } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { File, Folder } from "lucide-react-native";
-import { Theme } from "@/styles/theme";
+import type { Theme } from "@/styles/theme";
import { getAutocompleteScrollOffset } from "./autocomplete-utils";
export interface AutocompleteOption {
diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx
index 7a15e50ac..3bf33e278 100644
--- a/packages/app/src/components/ui/combobox.tsx
+++ b/packages/app/src/components/ui/combobox.tsx
@@ -101,7 +101,21 @@ function toNumericStyleValue(value: unknown): number | null {
}
function ComboboxSheetBackground({ style }: BottomSheetBackgroundProps) {
- return ;
+ const { theme } = useUnistyles();
+
+ return (
+
+ );
}
export interface SearchInputProps {
@@ -268,6 +282,7 @@ export function Combobox({
anchorRef,
children,
}: ComboboxProps): ReactElement {
+ const { theme } = useUnistyles();
const isMobile = useIsCompactFormFactor();
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
@@ -714,12 +729,12 @@ export function Combobox({
enableDismissOnClose={enableDismissOnClose}
stackBehavior={stackBehavior}
backgroundComponent={ComboboxSheetBackground}
- handleIndicatorStyle={styles.bottomSheetHandle}
+ handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
>
- {title}
+ {title}
{stickyHeader}
{!children && searchable ? searchInput : null}
@@ -909,14 +924,6 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
- bottomSheetBackground: {
- backgroundColor: theme.colors.surface0,
- borderTopLeftRadius: theme.borderRadius["2xl"],
- borderTopRightRadius: theme.borderRadius["2xl"],
- },
- bottomSheetHandle: {
- backgroundColor: theme.colors.palette.zinc[600],
- },
bottomSheetHeader: {
paddingHorizontal: theme.spacing[6],
paddingBottom: theme.spacing[2],
diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx
index 26e9e527c..854813214 100644
--- a/packages/app/src/components/ui/context-menu.tsx
+++ b/packages/app/src/components/ui/context-menu.tsx
@@ -349,6 +349,7 @@ export function ContextMenuContent({
testID?: string;
}>): ReactElement | null {
const context = useContextMenuContext("ContextMenuContent");
+ const { theme } = useUnistyles();
const webScrollbarStyle = useWebScrollbarStyle();
const isMobile = useIsCompactFormFactor();
const useMobileSheet = isMobile && mobileMode === "sheet";
@@ -475,8 +476,14 @@ export function ContextMenuContent({
onChange={handleSheetChange}
backdropComponent={renderSheetBackdrop}
enablePanDownToClose
- backgroundStyle={styles.sheetBackground}
- handleIndicatorStyle={styles.sheetHandle}
+ backgroundStyle={[
+ styles.sheetBackground,
+ {
+ backgroundColor: theme.colors.surface0,
+ borderColor: theme.colors.border,
+ },
+ ]}
+ handleIndicatorStyle={[styles.sheetHandle, { backgroundColor: theme.colors.surface2 }]}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
>