On this page
OverviewEnvironment RequirementsI. Business Scenarios and Key Points OverviewScenario Selection GuideII. General Best Practices1. useWindowHeight — Get Safe One Screen Height2. Keyboard Auto-hide3. Single Container Scrolling on Focus [iOS]4. Popup Position Fixed Relative to External Page [Android]5. Prohibit Scrolling During Focus [iOS]Note on @ts-ignoreIII. Detailed Explanation of Each Scenario1. Browse / Lightweight Input Form + Keyboard Auto-hideAutomatically hide the keyboard when the page is scrolledUse useWindowHeight for page height [Android]Fix the position of the bottom button [Android]2. Multi-input Form + Fixed KeyboardBottom buttons fix to the bottom; keyboard appears, buttons float up automatically on Android [Android]Avoid double scrolling container [iOS]Maintain the keyboard's residence3. Popup Lightweight Input Form + Keyboard Auto-hideControlled focus inside the popup + onTouchMove to lose focusUse useWindowHeight to calculate the bottom button position [Android]Popup position fixed relative to external container [Android]4. Popup Multi-input Form + Fixed KeyboardAllow background to scroll by controlling PopupThe bottom buttons inside the popup automatically float up when the keyboard pops up [Android]Popup position fixed relative to external page [Android]5. Popup FocusProhibit page scrolling during popup input focus6. Fixed Card Height + One Screen Fixed + Fixed KeyboardUse useWindowHeight to set container height, strictly one screenDisable page scrolling during textarea focus [iOS]Position the bottom button absolutely within a one screen container7. Adaptive Card Height + Scroll Beyond One Screen + Fixed KeyboardContainer height using useWindowHeight; internal ScrollView occupies remaining spaceField textarea using autosize, card height changes with contentThe bottom button remains at the bottom outside of the ScrollViewIV. Frequently Asked Questions (FAQ)Q1: Why is the useableWindowHeight 0 on Android devices for the first time?Q2: Will lockScroll={false} cause the background page to scroll?Q3: Why can't the page scroll even after setting disableScroll: false?Q4: What are the main differences in focus scenarios between iOS and Android?Q5: When to use position: fixed and when to use position: absolute?

Overview

When a user clicks on the input box, the keyboard will pop up, which can easily lead to issues such as the page being compressed, bottom buttons floating, and inability to scroll if not handled properly. This article organizes a set of practices and runnable demos based on common business scenarios (such as full-screen forms, input within pop-ups, multiple input fields, etc.) according to the Tuya UED specifications, to facilitate direct reuse and avoid common pitfalls. Github demo code

Environment Requirements

DependencyMinimum Version
APP Version7.3.0 (ty.enablePageScroll and other APIs require this version)
@ray-js/smart-uiPlease use the latest version

Note: The APIs mentioned in this article, such as ty.enablePageScroll, are supported only in APP versions 7.3.0 and above; please ensure compatibility with lower versions.


I. Business Scenarios and Key Points Overview

IllustrationScenarioKey PointsKey API / Component
Browse / Lightweight Input Form + Keyboard Auto-hideFull-screen form, automatic keyboard dismissal on sliding, bottom buttons do not float upuseWindowHeight, onTouchMove, ScrollView
Multi-input Form + Fixed KeyboardMultiple input fields, fixed navigation bar, bottom buttons automatically float up on Androidposition: fixed, disableScroll
Popup Lightweight Input Form + Keyboard Auto-hidePopup + few input fields, automatic keyboard dismissal on sliding, bottom buttons do not float upPopup, useWindowHeight, onTouchMove
Popup Multi-input Form + Fixed KeyboardPopup + multiple input fields, bottom buttons automatically float up on AndroidPopup, lockScroll={false}, position: fixed
Popup FocusSystem-level input popup, prohibits scrolling during focusDialogInstance.input, ty.enablePageScroll
Fixed Card Height + One Screen Fixed + Fixed KeyboardOne screen layout, fixed height field, prohibits scrolling during focususeWindowHeight, position: absolute
Adaptive Card Height + Scroll Beyond One Screen + Fixed KeyboardScrollView scrolling, autosize textarea, button sticks to the bottomuseWindowHeight, ScrollView, autosize

Scenario Selection Guide

flowchart TD
    A["Is it inside a popup?"] -->|No| B["Number of input fields?"]
    A -->|Yes| C["Type of popup?"]
    B -->|"Single/Few"| D["Scenario 1: Browse / Lightweight Input + Keyboard Auto-hide"]
    B -->|Multiple| E["Scenario 2: Multi-input Form + Fixed Keyboard"]
    B -->|"Card Layout"| I["Does it exceed one screen?"]
    I -->|No| J["Scenario 6: Card Fixed Height + One Screen Fixed"]
    I -->|Yes| K["Scenario 7: Card Adaptive + Scroll Beyond One Screen"]
    C -->|"System Popup"| H["Scenario 5: Popup Focus"]
    C -->|"Custom Popup + Few Input Fields"| F["Scenario 3: Popup Lightweight Input + Keyboard Auto-hide"]
    C -->|"Custom Popup + Multiple Input Fields"| G["Scenario 4: Popup Multi-input Form + Fixed Keyboard"]

II. General Best Practices

The following general knowledge will be repeatedly used in various scenarios, and it is recommended to familiarize yourself with it beforehand.

1. useWindowHeight — Get Safe One Screen Height

100vh represents the height of the entire WebView container, but the WebView will be compressed when the keyboard pops up on Android devices, causing the bottom content to float up. To keep the page at full screen height during focus, a fixed pixel value needs to be obtained.

Notes:

  • The usable height of the page container can be obtained via getSystemInfoSync().useableWindowHeight.
  • On Android devices, during page initialization, the useableWindowHeight value is 0 (the logical layer asynchronously obtains the container height), so it needs to be acquired later.
  • It is recommended to acquire it during the first focus event.

Suggest wrapping it into a custom hook:

import { useState, useCallback } from 'react';
import { getSystemInfoSync } from '@ray-js/ray';

/**
 * Get the safe one screen height
 *
 * Why not use 100vh directly?
 * - The WebView container is compressed when the keyboard pops up on Android, making 100vh smaller.
 * - The useableWindowHeight returns a fixed physical pixel value, unaffected by the keyboard.
 *
 * Why not retrieve it during initialization?
 * - On Android, useableWindowHeight's initial value is 0 (asynchronously obtained),
 *   it needs to be retrieved during the first focus.
 */
function useWindowHeight(initialHeight = '100vh') {
  const [height, setHeight] = useState(initialHeight);

  const ensureHeight = useCallback(() => {
    if (height === initialHeight) {
      // useableWindowHeight is not included in the official TS type definitions, but is available at runtime
      const info = getSystemInfoSync() as Record<string, any>;
      const { useableWindowHeight } = info;
      if (useableWindowHeight) {
        setHeight(`${useableWindowHeight}px`);
      }
    }
  }, [height, initialHeight]);

  return { height, ensureHeight };
}

Subsequent scenario codes will directly use useWindowHeight, without repeating the retrieval logic.

2. Keyboard Auto-hide

By using the controlled focus of the Input component + calling setFocus(false) on the container's onTouchMove.

<Input
  focus={focus}
  onFocus={() => setFocus(true)}
  onBlur={() => setFocus(false)}
  placeholder="Please enter content"
/>

<View onTouchMove={() => focus && setFocus(false)}>
  {/* Page content */}
</View>

Note: If there are behaviors that prevent bubbling inside, the onTouchMove event may not trigger.

3. Single Container Scrolling on Focus [iOS]

During focus, iOS defaults to wrap the outer layer of the WebView with a scrolling container. If an internal ScrollView is also used, it creates a double-layer scroll container—the page may get stuck when scrolling too fast.

Solution: Use page-level scrolling, set in index.config.ts:

export default {
  // ...
  disableScroll: false,
};

At this point, the top navigation, bottom buttons, etc., need to be positioned using fixed, and the content area should leave padding for scrolling.

4. Popup Position Fixed Relative to External Page [Android]

When focused in a popup scenario on Android devices, the root node can use 100vh as a calculation baseline to allow the container to adaptively compress, keeping the popup's vertical position unchanged.

.popupContainer {
  height: calc(100vh - 50px);
}

iOS's focus position is uncontrollable, this solution is only valid for Android.

5. Prohibit Scrolling During Focus [iOS]

On iOS, scrolling is allowed by default during focus and cannot be controlled. In APP versions 7.3.0 and above, you can call the following in the onFocus callback:

// enablePageScroll is available in newer version type definitions
// If the current type definitions do not include it, it can be resolved through module extension (see below for explanation)
ty.enablePageScroll?.({ scrollEnable: false });

Note: Once scrolling is set to be disabled, it will also be effective in non-focused scenarios. It is recommended to restore it in onBlur:

ty.enablePageScroll?.({ scrollEnable: true });

Note on @ts-ignore

Some API calls in the documentation may encounter TypeScript type errors for the following reasons:

APIReasonSuggestion
getSystemInfoSync().useableWindowHeightThis field is not included in the official TS type definitions, but is available at runtimeUse type assertion as Record<string, any>
ty.enablePageScrollThis API is only supported in APP 7.3.0+, type definitions may not be coveredUse optional chaining ?. to call it

Recommended Approach: Extend type definitions through module declaration to avoid directly using @ts-ignore:

types/ray-js.d.ts
// types/ray-js.d.ts
declare module '@ray-js/ray' {
  interface SystemInfo {
    useableWindowHeight?: number;
  }
}

III. Detailed Explanation of Each Scenario

1. Browse / Lightweight Input Form + Keyboard Auto-hide

Automatically hide the keyboard when the page is scrolled

Utilizing the focus property of the Input component + onTouchMove of the container, the input field automatically loses focus to hide the keyboard when scrolling the page. (See "General Best Practices — Keyboard Auto-hide")

Use useWindowHeight for page height [Android]

Use the useWindowHeight hook to get the accurate one screen height, preventing the page from compressing when the keyboard pops up on Android devices. (See "General Best Practices — useWindowHeight")

const { height, ensureHeight } = useWindowHeight();

const onInputFocus = () => {
  ensureHeight();
  setFocus(true);
};

Fix the position of the bottom button [Android]

Do not use position: fixed; otherwise, the button will float up when the keyboard pops up on Android.

You can use a fixed height for the root node + position: relative, and the button area can use position: absolute; or use a flex adaptive layout to fill the full screen.

<View className={styles.container} style={{ height }} onTouchMove={() => focus && setFocus(false)}>
  <NavBar />
  <ScrollView scrollY className={styles.content}>
    {/* Form content */}
  </ScrollView>
  <View className={styles.buttonContainer}>{/* Bottom button */}</View>
</View>
.container {
  display: flex;
  flex-direction: column;
  .content {
    flex: 1;
  }
  .buttonContainer {
    height: 192rpx;
  }
}

2. Multi-input Form + Fixed Keyboard

Bottom buttons fix to the bottom; keyboard appears, buttons float up automatically on Android [Android]

Fixed positioning for the bottom operation area using position: fixed, which does not scroll with content. Remember to leave bottom padding for the scroll container to prevent the bottom button from blocking the scroll content.

.buttonContainer {
  position: fixed;
  bottom: 0;
}

Avoid double scrolling container [iOS]

iOS defaults to wrap the outer layer of the mini-program container with a scrolling container during focus; if the internal scroll container also exists, double scrolling is easily encountered—scrolling too fast may cause the page to get stuck. It is recommended to use page-level scrolling. (See "General Best Practices — Single Container Scrolling on Focus")

Set in index.config.ts:

export default {
  // ...
  disableScroll: false,
};

If using a custom navigation bar, set position: fixed to stick to the top, and the content area should use CSS variables to leave statusBar + navbar height to avoid being blocked:

<View className={styles.container}>
  <NavBar className={styles.navBar} />
  <View className={styles.content}>{/* Form content */}</View>
  <View className={styles.buttonContainer}>{/* Bottom button */}</View>
</View>
.navBar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
}
.content {
  padding-top: calc(var(--app-device-status-height, 20px) + var(--app-device-navbar-height, 46px));
  padding-bottom: 192rpx;
}

Maintain the keyboard's residence

No additional handling is required, conforming to the system's default behavior.

3. Popup Lightweight Input Form + Keyboard Auto-hide

Controlled focus inside the popup + onTouchMove to lose focus

This is consistent with Scenario 1's "Keyboard Auto-hide." Inside the Popup, use ScrollView + controlled focus, and listen for onTouchMove on the popup content container to lose focus and hide the keyboard during focus.

<Popup position="bottom" round show={showPopup} closeable onClose={() => setShowPopup(false)}>
  <View className={styles.popupContainer} onTouchMove={() => focus && setFocus(false)}>
    <ScrollView scrollY className={styles.popupContent}>
      <Input
        focus={focus}
        onFocus={() => setFocus(true)}
        onBlur={() => setFocus(false)}
        placeholder="Please enter content"
      />
    </ScrollView>
  </View>
</Popup>

Use useWindowHeight to calculate the bottom button position [Android]

Use useWindowHeight on Android devices to get the height and calculate the button top, ensuring it remains fixed relative to the visible area.

const { height, ensureHeight } = useWindowHeight();

const onInputFocus = () => {
  ensureHeight();
  setFocus(true);
};

// Button container
<View
  className={styles.buttonContainer}
  style={{
    top: `calc(${height} - 192rpx - 50px)`,
  }}
>
  {/* Bottom button */}
</View>;

Do not set fixed px or rpx values as this will cause the popup to be pushed up when the keyboard appears on Android devices. Setting a height of calc(100vh - 50px) allows the container to adaptively compress. (See "General Best Practices — Popup Position Fixed Relative to External Page")

<View className={styles.container}>
  <NavBar />
  <Popup position="bottom">
    <View className={styles.popupContainer}>
      <ScrollView scrollY className={styles.popupContent}>
        {/* Popup content */}
      </ScrollView>
    </View>
  </Popup>
</View>
.popupContainer {
  height: calc(100vh - 50px);
}

4. Popup Multi-input Form + Fixed Keyboard

Allow background to scroll by controlling Popup

Use lockScroll={false} to enable background scrolling. In keyboard-focused scenarios, users can slide slowly to scroll the outer container.

<Popup lockScroll={false} position="bottom">
  {/* Popup content */}
</Popup>

The bottom buttons inside the popup automatically float up when the keyboard pops up [Android]

On Android devices, set the layout with position: fixed, allowing the bottom buttons to float just above the keyboard when it pops up. Remember to leave space at the bottom of the scroll container to prevent content from being blocked by the button.

.buttonContainer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
}

Do not set fixed px or rpx values; this will cause the entire popup to be pushed up when the keyboard appears on Android devices. Setting a height of calc(100vh - 50px) allows the container to adaptively compress. (See "General Best Practices — Popup Position Fixed Relative to External Page")

<View className={styles.container}>
  <NavBar />
  <Popup position="bottom">
    <View className={styles.popupContainer}>
      <ScrollView scrollY className={styles.popupContent}>
        {/* Popup content */}
      </ScrollView>
    </View>
  </Popup>
</View>
.popupContainer {
  height: calc(100vh - 50px);
}

5. Popup Focus

Prohibit page scrolling during popup input focus

Solution A: Using SmartUI Dialog Component (Recommended)

Call SmartUI's DialogInstance.input system-level input popup, as its internal setup already disables the page scroll, requiring no additional handling.

Common parameter example:

DialogInstance.input({
  title: 'Please enter',
  placeholder: 'Input content',
  maxlength: 20,
  onConfirm: value => {
    console.log('User input:', value);
  },
  onCancel: () => {
    console.log('User canceled');
  },
});

Solution B: Using Custom Popup

If using a custom popup, you need to manually manage the page scrolling state:

// Trigger popup appearance
const showDialog = () => {
  // Disable page scrolling (Supported in APP 7.3.0+)
  ty.enablePageScroll?.({ scrollEnable: false });
  // Popup logic...
};

// Callback for popup closing
const closeMyDialog = () => {
  // Restore page scrolling
  ty.enablePageScroll?.({ scrollEnable: true });
  // Close popup logic...
};

Note: It is essential to restore scrolling after closing the popup; otherwise, the page will be unscrollable.

6. Fixed Card Height + One Screen Fixed + Fixed Keyboard

Use useWindowHeight to set container height, strictly one screen

Set the container height through useWindowHeight, avoiding the use of 100vh, which causes the container to compress when the keyboard appears.

const { height, ensureHeight } = useWindowHeight();

const onTextareaFocus = () => {
  ensureHeight();
  // Disable page scrolling (Supported in APP 7.3.0+)
  ty.enablePageScroll?.({ scrollEnable: false });
};
<View className={styles.container} style={{ height }}>
  {/* Page content */}
</View>

Disable page scrolling during textarea focus [iOS]

Call ty.enablePageScroll({ scrollEnable: false }) in the Field's onFocus to prevent the iOS system's default scroll behavior during focus.

<Field type="textarea" onFocus={onTextareaFocus} placeholder="Please enter your message" />

Position the bottom button absolutely within a one screen container

Use position: absolute + bottom for buttons, positioning them relative to the root container with a fixed height to prevent floating up when the keyboard appears.

.container {
  position: relative;
  /* Height is actually injected by style={{ height }} from useWindowHeight */
}
.button {
  position: absolute;
  bottom: 112rpx;
  left: 50%;
  transform: translateX(-50%);
}

7. Adaptive Card Height + Scroll Beyond One Screen + Fixed Keyboard

Container height using useWindowHeight; internal ScrollView occupies remaining space

Use useWindowHeight for the outer container with fixed height, the inner ScrollView scrollY uses flex: 1 to fill and enable scrolling within one screen.

const { height, ensureHeight } = useWindowHeight();

const onTextareaFocus = () => {
  ensureHeight();
};
<View className={styles.container} style={{ height }}>
  <NavBar />
  <ScrollView scrollY className={styles.content}>
    {/* Card list */}
  </ScrollView>
  <View className={styles.buttonContainer}>{/* Bottom button */}</View>
</View>
.container {
  display: flex;
  flex-direction: column;
  .content {
    flex: 1;
  }
  .buttonContainer {
    height: 192rpx;
  }
}

Field textarea using autosize, card height changes with content

Control multi-line input height with autosize's maxHeight / minHeight to avoid overly fixed heights that lead to content being clipped or excessive whitespace.

import { Field } from '@ray-js/smart-ui';

<Field
  type="textarea"
  hiddenLabel
  autosize={{
    maxHeight: '436rpx',
    minHeight: '80rpx',
  }}
  placeholder="Please enter your message"
/>;

The bottom button remains at the bottom outside of the ScrollView

Place the button container outside the ScrollView, inside the root container, using flex or fixed height to ensure it stays at the bottom when the keyboard appears and remains in view.


IV. Frequently Asked Questions (FAQ)

Q1: Why is the useableWindowHeight 0 on Android devices for the first time?

On Android, the logical layer asynchronously retrieves the container height, and it has not completed this retrieval during page initialization. It is recommended to retrieve it during the first focus event when the container height is ready.

Q2: Will lockScroll={false} cause the background page to scroll?

Yes. After setting lockScroll={false}, slow dragging by the user may trigger scrolling of the outer page. If this behavior is undesirable, please keep the default lockScroll={true}, but be aware that this may affect keyboard focus experience within the popup.

Q3: Why can't the page scroll even after setting disableScroll: false?

Check the following points:

  1. Whether the content height exceeds the viewport height
  2. Whether ty.enablePageScroll?.({ scrollEnable: false }) was called elsewhere without being restored
  3. Whether CSS styles like overflow: hidden prevent scrolling

Q4: What are the main differences in focus scenarios between iOS and Android?

BehavioriOSAndroid
Container change on keyboard pop-upDoes not compress WebViewCompresses WebView (100vh becomes smaller)
Page scroll during focusDefaults to scrollable (can be disabled in 7.3.0+)Defaults to not scrollable
Buttons with position: fixedPosition remains fixedFloat up with the keyboard
Outer scrolling containerAutomatically wrapped by the systemNone

Q5: When to use position: fixed and when to use position: absolute?

  • fixed: Used in scenarios where the button is expected to float with the keyboard (such as scenarios 2 and 4), positioning the button relative to the viewport.
  • absolute: Used in scenarios where the button position is expected to remain the same (such as scenarios 1 and 6), positioning the button relative to a fixed height parent container.