Skip to content

KeyboardAwareScrollView 1.21.1 doesn't work as expected #1394

@kirillzyusko

Description

@kirillzyusko

Discussed in #1390

Originally posted by grego5 March 22, 2026

On initial input focus there's some inaccuracy in the scrolling offset, which corrects on second focus attempt, but every time you focus a different input, it's doesn't go all the way as expected. Especially noticeable for multiline inputs.
When there's not enough space at the bottom for scrolling, well, it doesn't scroll when keyboard appears. That means I always need to add at least a keyboard size spacer at the bottom, fixed, since the point of the new scrollview that we don't resize the containers. I thought the new technique is that it adds some special padding internally that let content clip and extend the scrollable limits temporarily, so I don't need to do layout manipulations.

Screen_Recording_20260322_231637_Keyboard.Controller.Demo.mp4

repo with apk included.
https://github.com/grego5/rnkbctrldemo

Unrelated to the problem, but in the demo I probably confused the sticky view offset. They just happen to match, so visually it works on my samsung, like (ACTION_BAR_MARGIN * 2 = insets.bottom = 20), but generally safe area insets should not be used there, different math is appropriate, like in the snippet at the bottom from my main project.

Also I haven't noticed extraKeyboardSpace makes any difference.

    <SafeAreaView style={styles.screen}>
      <View style={styles.header}>
        <Text style={styles.title}>Keyboard Controller Demo</Text>
        <Text style={styles.subtitle}>
          Plain React Native layout for testing KeyboardAwareScrollView and
          KeyboardStickyView.
        </Text>
      </View>

      <KeyboardAwareScrollView
        // @ts-ignore probably rngh 3 beta types bug
        ScrollViewComponent={ScrollView}
        bottomOffset={ACTION_BAR_HEIGHT + ACTION_BAR_MARGIN * 2}
        style={styles.scroll}
        contentContainerStyle={styles.scrollContent}
        keyboardShouldPersistTaps="handled"
        showsVerticalScrollIndicator={false}>
        {singleLineFields.map((field) => (
          <Field
            key={field.key}
            label={field.label}
            value={form[field.key]}
            inputIndex={orderedFields.indexOf(field.key)}
            setRef={registerInput}
            onFocus={setFocusedIndex}
            onSubmitEditing={() => moveFocus(1)}
            onChangeText={(value) => updateField(field.key, value)}
          />
        ))}

        {multiLineFields.map((field) => (
          <Field
            key={field.key}
            label={field.label}
            value={form[field.key]}
            minHeight={field.minHeight}
            multiline
            inputIndex={orderedFields.indexOf(field.key)}
            setRef={registerInput}
            onFocus={setFocusedIndex}
            onChangeText={(value) => updateField(field.key, value)}
          />
        ))}
      </KeyboardAwareScrollView>

      <KeyboardStickyView offset={{ opened: insets.bottom }}>
        <View style={styles.toolbar}>
          <ToolbarButton label="Prev" onPress={() => moveFocus(-1)} />
          <ToolbarButton label="Next" onPress={() => moveFocus(1)} />
          <ToolbarButton label="Save" />
          <ToolbarButton label="Quick" />
        </View>
      </KeyboardStickyView>
    </SafeAreaView>

I wanted to upgrade this pattern but seems too early. (The animated view smoothes the layout shift transitions).

const keyboardStickyViewOffset = { opened: ACTION_BAR_HEIGHT - ACTION_BAR_MARGIN * 2 };

          <KeyboardAvoidingView
            style={$styles.grow}
            behavior='padding'
            keyboardVerticalOffset={ACTION_BAR_HEIGHT}
          >
            <Animated.View style={$styles.grow} layout={LinearTransition}>
              <ScrollView
                keyboardShouldPersistTaps='handled'
                showsVerticalScrollIndicator={false}
                style={$styles.flexContainer}
                contentContainerStyle={$styles.container}
              >
                {endCustomer.uid && (
                  <RadioButton.Group
                    onValueChange={(value) => {
                      const name = value === 'end_customer' ? endCustomer.name : customer.name;
                      form.setValue('subject', name, { shouldDirty: true });
                    }}
                    value={customerType}
                  >
                    <RadioButton.Item label={endCustomer.name} value='end_customer' />
                    <RadioButton.Item label={customer.name} value='customer' />
                  </RadioButton.Group>
                )}

                <ReportEditorFields
                  form={form}
                  isRestricted={isRestricted}
                  device={device}
                  openSearch={openSearch}
                />
              </ScrollView>
            </Animated.View>
          </KeyboardAvoidingView>

          <KeyboardStickyView offset={keyboardStickyViewOffset} style={$styles.rowSpacedEven}>
            <IconButton
              mode='contained'
              loading={isLoading}
              onPress={submitDialogControl.open}
              icon={props.isEditMode ? 'pencil' : 'check-bold'}
              accessibilityLabel={props.isEditMode ? Labels.Update : Labels.Confirm}
            />

            <IconButton
              mode='contained'
              onPress={quickSertControl.open}
              icon='format-list-bulleted'
              accessibilityLabel={Labels.Add}
            />

            <IconButton
              mode='contained'
              onPress={() => {
                if (usageReportFile) fileManager.deleteFile(usageReportFile);
                const draft = {
                  ...form.getValues(),
                  status: '',
                  usage_counter: '',
                  usage_report: '',
                  ticket_uid: undefined,
                  ticket_number: undefined,
                };
                reportDraftsManager.setTemp(draft);
                router.setParams({ ticketId: undefined, reportId: undefined });
              }}
              icon='content-copy'
              accessibilityLabel={Labels.Copy}
              disabled={!isRestricted}
            />

            <IconButton
              disabled={!form.formState.isDirty}
              mode='contained'
              onPress={() => form.reset()}
              icon='restore'
              accessibilityLabel={Labels.Reset}
            />
          </KeyboardStickyView>
```</div>

Metadata

Metadata

Assignees

Labels

KeyboardAwareScrollView 📜Anything related to KeyboardAwareScrollView componentrepro providedIssue contains reproduction repository/code🐛 bugSomething isn't working🤖 androidAndroid specific

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions