Skip to main content
← Blog

Wait, Which Field Am I Editing?

VauDium ·

Implementing focus highlighting for editable fields in a document-style task editor.

Wait, Which Field Am I Editing?

Too Many Sections

The task editing screen in Fecit looks like a document. Description, intention (Target/Expectation), preparation items, review, labels, project… there are a lot of editable sections in a single task. When you’re scrolling through and typing, there’s this moment where you think, “Wait, which field am I actually editing right now?”

The cursor is blinking somewhere, so technically you know. But visually, there’s no strong signal that a particular section is “active.” This gets worse on mobile when the keyboard comes up and cuts the visible area in half.

The goal was simple: clearly highlight the field being edited. The journey to get there, though, involved more trial and error than expected.

Iteration: Starting with Borders

Attempt 1: Border + Shadow

The first try was the classic focus style. Blue border with a shadow. The result? Way too heavy. Fecit’s editing screens have a minimal, document-like feel. Suddenly slapping a border and shadow on one section made it look like a completely different app.

Attempt 2: Border Only

Dropped the shadow, kept just the border. Not bad, actually. But still a bit much. Borders give off an “I am a form field” vibe, and Fecit’s editor is designed so that field boundaries don’t stand out. The directions didn’t align.

Attempt 3: Border + Background Color

Made the border more subtle and added a light background color. Getting better, but there was still the layout shift problem. When a border appears on focus, the layout shifts, and that shift causes the native input to remount and lose focus. Not great.

“Just keep a transparent border all the time and only change the color.” Sure, that works mechanically, but maintaining an invisible border felt like an unnecessary visual element. Once you start adjusting border-radius and margins to make it look right, the whole document feel falls apart.

Final Answer: Background Color Only

The simplest approach turned out to be the right one. Change the focused section’s background to PRIMARY50 (a very light blue), and change the label and icon colors to PRIMARY500. That’s it.

No border, no shadow, no layout change. Only colors change, so the layout stays exactly the same. This was the key insight.

The Core Constraint: Layout Must Not Change

When implementing focus highlighting in React Native, there’s one critical constraint: focus state changes must not alter layout. If the layout changes, the native input component remounts. If it remounts, focus is lost. If focus is lost, the highlight disappears. It’s an infinite loop.

That ruled out any border, margin, or padding changes. Only color changes are safe. Background color, text color, icon color. Those three, nothing else.

FocusHighlightContainer

The final implementation uses a render prop pattern in a container component.

export const FocusHighlightContainer: FC<{
    children: (onFocusChange: (focused: boolean) => void, focused: boolean) => ReactNode;
    externalFocused?: boolean;
}> = ({children, externalFocused}) => {
    const palette = usePalette();
    const [internalFocused, setInternalFocused] = useState(false);
    const focused = externalFocused ?? internalFocused;

    return (
        <View style={{
            backgroundColor: focused ? palette.PRIMARY50 : undefined,
        }}>
            {children(setFocused, focused)}
        </View>
    );
};

children is a function. It receives onFocusChange and focused, and uses them however it needs to. Usage looks like this:

<FocusHighlightContainer>
    {(onFocusChange, focused) => (
        <SomeSection>
            <Label color={focused ? palette.PRIMARY500 : palette.NEUTRAL500} />
            <TextInput
                onFocus={() => onFocusChange(true)}
                onBlur={() => onFocusChange(false)}
            />
        </SomeSection>
    )}
</FocusHighlightContainer>

Why useMemo Breaks Everything

Initially, I tried managing the focus state in the parent component. The screens were using a pattern where sections are created inside useMemo. When I added focus state as a dependency to that memo, things broke.

Focus changes -> useMemo recalculates -> section components are recreated -> native input remounts -> focus is lost. Exactly the problem I was trying to avoid.

The fix was straightforward. Don’t manage focus state in the parent. Let FocusHighlightContainer manage its own state internally. The component has its own useState, changes its own background color. It’s completely decoupled from parent re-renders.

Extending to Modal Pickers

Text input fields are handled cleanly with onFocus/onBlur. But Fecit also has plenty of modal-based pickers. Satisfaction level, difficulty level, things like that. These aren’t text inputs — you tap them and a modal opens.

For these pickers, I used an onOpenChange callback. When the modal opens, call onFocusChange(true). When it closes, call onFocusChange(false).

<FocusHighlightContainer>
    {(onFocusChange, focused) => (
        <SatisfactionPicker
            onOpenChange={(open) => onFocusChange(open)}
            labelColor={focused ? palette.PRIMARY500 : palette.NEUTRAL500}
        />
    )}
</FocusHighlightContainer>

While the modal is open, the section stays highlighted. When the modal closes, you can immediately see which item you were editing. It feels natural.

Project Picker: externalFocused

Some pickers, like the project picker, already manage their open/closed state externally. Having FocusHighlightContainer maintain a second copy of that state would be redundant.

That’s what the externalFocused prop is for. When provided, the internal useState is ignored, and the externally provided value is used directly. You just wire up the existing modal state.

const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);

<FocusHighlightContainer externalFocused={isProjectPickerOpen}>
    {(_, focused) => (
        <ProjectPicker
            isOpen={isProjectPickerOpen}
            onToggle={() => setIsProjectPickerOpen(!isProjectPickerOpen)}
            labelColor={focused ? palette.PRIMARY500 : palette.NEUTRAL500}
        />
    )}
</FocusHighlightContainer>

Applied Across 30+ Screens

This pattern was applied across every edit and create screen in the app. Task template creation, task record creation, subtask editing, preparation item editing, overview creation, label creation, project creation, daily routines, weekly reviews, freeboards… over 30 screens.

A single component providing a consistent focus experience across all of them — that’s the power of the render prop pattern. Each screen has a different layout and different internal elements, but wrapping with FocusHighlightContainer and connecting onFocusChange gives you the same highlight behavior everywhere.

Looking Back

I started thinking “it’s a focus indicator, just add a border.” But native inputs in React Native are sensitive to layout changes, and borders didn’t fit the document-style editor design anyway.

The best answer ended up being the lightest one. One background color, one text color. The Minimal to Maximal principle applied even at the level of UI details. It’s a small change, but when you’re editing a task, the confidence of “I’m editing right here” is unmistakable.