@launchhq/react-native-keyboard-composer
v0.1.3
Published
A native keyboard-aware composer component for React Native with smooth keyboard animations
Maintainers
Readme
@launchhq/react-native-keyboard-composer
A native keyboard-aware composer for React Native chat applications. Built specifically for AI chat interfaces like ChatGPT and v0, where content needs to react intelligently to keyboard and input changes.
Demo
Click to watch demo · Smooth keyboard animations with auto-growing input and content-aware positioning.
The Problem
In chat applications, keyboard handling is notoriously difficult:
- When should content push up vs the keyboard overlay content?
- How do you maintain the gap between the last message and composer as the input grows?
- What happens when the user scrolls while the keyboard is open, then closes it?
This library solves all of that with native implementations that handle the edge cases.
Features
- 💬 Built for chat UIs - Content reacts correctly to keyboard open/close
- 📏 Smart content positioning - Knows when to push content up vs overlay
- ⌨️ Auto-growing input - Composer expands with text, content adjusts accordingly
- 🔄 Scroll-to-bottom button - Appears when you scroll away from latest messages
- 📱 iOS & Android parity - Same behavior on both platforms
- 🎛️ Streaming support - Built-in stop button for AI streaming responses
- 🌙 Dark mode support - Automatically adapts to system theme
- 👆 Gesture support (iOS) - Swipe down to dismiss keyboard, swipe up to open
Installation
pnpm add @launchhq/react-native-keyboard-composer
# or
npm install @launchhq/react-native-keyboard-composer
# or
yarn add @launchhq/react-native-keyboard-composerFor Expo managed projects, run:
npx expo prebuildUsage
Basic Example
import {
KeyboardComposer,
KeyboardAwareWrapper,
} from "@launchhq/react-native-keyboard-composer";
function ChatScreen() {
const [composerHeight, setComposerHeight] = useState(48);
return (
<KeyboardAwareWrapper style={{ flex: 1 }} extraBottomInset={composerHeight}>
<ScrollView>{/* Your chat messages */}</ScrollView>
<View style={styles.composerContainer}>
<KeyboardComposer
placeholder="Type a message..."
onSend={(text) => handleSend(text)}
onHeightChange={(height) => setComposerHeight(height)}
onComposerFocus={() => console.log("Focused")}
onComposerBlur={() => console.log("Blurred")}
/>
</View>
</KeyboardAwareWrapper>
);
}With AI Streaming
import { KeyboardComposer } from "@launchhq/react-native-keyboard-composer";
function AIChat() {
const [isStreaming, setIsStreaming] = useState(false);
const handleSend = async (text: string) => {
setIsStreaming(true);
await streamAIResponse(text);
setIsStreaming(false);
};
return (
<KeyboardComposer
placeholder="Ask anything..."
isStreaming={isStreaming}
onSend={handleSend}
onStop={() => cancelStream()}
/>
);
}Dismissing Keyboard Programmatically
const [blurTrigger, setBlurTrigger] = useState(0);
// Call this to dismiss keyboard
const dismissKeyboard = () => setBlurTrigger(Date.now());
<KeyboardComposer
blurTrigger={blurTrigger}
// ...other props
/>;API Reference
<KeyboardComposer />
The main composer input component.
| Prop | Type | Default | Description |
| ------------------------ | -------------------------- | --------------------- | ----------------------------------- |
| placeholder | string | "Type a message..." | Placeholder text |
| minHeight | number | 48 | Minimum height in dp/points |
| maxHeight | number | 120 | Maximum height before scrolling |
| sendButtonEnabled | boolean | true | Whether send button is enabled |
| editable | boolean | true | Whether input is editable |
| autoFocus | boolean | false | Auto-focus on mount |
| blurTrigger | number | - | Change value to trigger blur |
| isStreaming | boolean | false | Shows stop button when true |
| onChangeText | (text: string) => void | - | Called when text changes |
| onSend | (text: string) => void | - | Called when send is pressed |
| onStop | () => void | - | Called when stop is pressed |
| onHeightChange | (height: number) => void | - | Called when height changes |
| onKeyboardHeightChange | (height: number) => void | - | Called when keyboard height changes |
| onComposerFocus | () => void | - | Called when input gains focus |
| onComposerBlur | () => void | - | Called when input loses focus |
| style | StyleProp<ViewStyle> | - | Container style |
<KeyboardAwareWrapper />
Wrapper component that handles keyboard-aware scrolling.
| Prop | Type | Default | Description |
| -------------------- | ---------------------- | ------- | ---------------------------------------------------- |
| pinToTopEnabled | boolean | false | Enables pin-to-top + runway behavior (see below) |
| extraBottomInset | number | 0 | Bottom inset (typically the current composer height) |
| scrollToTopTrigger | number | 0 | Change value to arm pin-to-top for the next append |
| style | StyleProp<ViewStyle> | - | Container style |
| children | ReactNode | - | Should contain a ScrollView |
Pin-to-top behavior (optional)
Pin-to-top is opt-in and is controlled via KeyboardAwareWrapper (not KeyboardComposer).
When pinToTopEnabled is true:
- The next user message append is pinned to the top of the viewport.
- A non-scrollable runway is created below it so streamed assistant responses can grow without the content snapping around.
- While streaming grows content, the wrapper keeps the pinned position stable unless the user manually scrolls away.
When pinToTopEnabled is false (or omitted), the wrapper behaves like a normal keyboard-aware chat wrapper (no runway/pinning).
You can toggle pinToTopEnabled at runtime; disabling it clears any active runway/pin state.
scrollToTopTrigger
Despite the name, scrollToTopTrigger is used to arm pin-to-top for the next content append (use a counter or Date.now()).
constants
Module constants for default values:
import { constants } from "@launchhq/react-native-keyboard-composer";
console.log(constants.defaultMinHeight); // 48
console.log(constants.defaultMaxHeight); // 120
console.log(constants.contentGap); // 32Styling & Customization
Built-in Spacing
The library automatically handles spacing between your content and the composer:
| Constant | iOS (pt) | Android (dp) | Description |
| ----------------------- | -------- | ------------ | ------------------------------------- |
| CONTENT_GAP | 24 | 24 | Gap between last message and composer |
| COMPOSER_KEYBOARD_GAP | 8 | 8 | Gap between composer and keyboard |
Note: While both platforms use the same numerical values, the visual spacing may appear different due to how each platform handles safe areas, scroll content insets, and keyboard positioning. iOS typically shows more visible gap due to its safe area and scroll inset calculations.
Adding Extra Spacing
If you need more space between your content and the composer, add paddingBottom to your scroll content:
<ScrollView
contentContainerStyle={{
paddingBottom: 16, // Extra space above composer
}}
>
{/* Your messages */}
</ScrollView>Composer Container Styling
The KeyboardComposer should be placed inside KeyboardAwareWrapper with absolute positioning for proper keyboard animation:
<KeyboardAwareWrapper style={{ flex: 1 }} extraBottomInset={composerHeight}>
<ScrollView>{/* Content */}</ScrollView>
{/* Composer - positioned absolutely, animated by native code */}
<View style={styles.composerContainer}>
<View style={[styles.composerWrapper, { height: composerHeight }]}>
<KeyboardComposer ... />
</View>
</View>
</KeyboardAwareWrapper>
const styles = StyleSheet.create({
composerContainer: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 16,
paddingBottom: 16, // Or use safe area insets
},
composerWrapper: {
borderRadius: 24,
backgroundColor: '#F2F2F7',
overflow: 'hidden',
},
});How It Works
The library handles three key scenarios:
- Keyboard opens - Content pushes up to keep the last message visible above the composer
- Input grows/shrinks - As you type multiple lines, content scrolls to maintain the gap between your last message and the composer
- Keyboard closes - If you scrolled while the keyboard was open, content adjusts to prevent awkward gaps
Technical Details
- iOS: Uses
keyboardLayoutGuide(iOS 15+) withCADisplayLinkfor frame-accurate positioning - Android: Uses
WindowInsetsAnimationCompatfor synchronized keyboard tracking
Platform Support
| Platform | Support | | -------- | ------------------------ | | iOS | ✅ Native implementation | | Android | ✅ Native implementation | | Web | ❌ Not supported |
Gestures (iOS)
The composer supports intuitive swipe gestures on the input field:
| Gesture | Action | | ---------- | ---------------------------------------- | | Swipe down | Dismisses the keyboard | | Swipe up | Focuses the input and opens the keyboard |
These gestures provide a natural way to control the keyboard without reaching for the keyboard dismiss button or tapping outside.
Requirements
- React Native 0.71+
- Expo SDK 48+ (for Expo projects)
- iOS 15+
- Android API 21+
Development
If you’re contributing to this repo (or running the example/ app locally), see CONTRIBUTING.md for a clear breakdown of:
- Running the example against the published npm package (consumer mode)
- Running the example against the local package source (native development mode)
Local Development Setup
To test this package locally in another project:
Option A: Using npm/yarn (Simple)
This approach works with any package manager and doesn't require workspaces.
1. Link to the local package in your package.json:
{
"dependencies": {
"@launchhq/react-native-keyboard-composer": "file:../react-native-keyboard-composer"
}
}Adjust the path to point to where you cloned the package.
2. Configure Metro to watch the external package:
In your app's metro.config.js:
const path = require("path");
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
// Path to the local package
const keyboardComposerPath = path.resolve(
__dirname,
"../react-native-keyboard-composer" // Adjust path as needed
);
// Watch the external package folder for changes
config.watchFolders = [keyboardComposerPath];
// Map the package name to the local path
config.resolver.extraNodeModules = {
"@launchhq/react-native-keyboard-composer": keyboardComposerPath,
};
module.exports = config;3. Install dependencies:
npm install
# or
yarn install4. Rebuild native code (required for native modules):
npx expo prebuild --clean
npx expo run:ios
# or
npx expo run:androidNow any changes to the package will be reflected immediately in your app.
Option B: Using pnpm Workspaces (Monorepo)
If you're using pnpm workspaces in a monorepo setup:
1. Add the package to your workspace:
In your project's pnpm-workspace.yaml:
packages:
- apps/*
- ../react-native-keyboard-composer # Adjust path as needed2. Use workspace protocol in package.json:
{
"dependencies": {
"@launchhq/react-native-keyboard-composer": "workspace:*"
}
}3. Configure Metro (same as Option A step 2 above)
4. Install dependencies:
pnpm installNow any changes to the package will be reflected immediately in your app.
Publishing to npm
When you're ready to publish:
1. Build and publish the package
cd react-native-keyboard-composer
pnpm run build
npm publish --access public2. Update consuming apps to use the published version
In the consuming app's package.json, change:
{
"dependencies": {
// From:
"@launchhq/react-native-keyboard-composer": "workspace:*"
// To:
"@launchhq/react-native-keyboard-composer": "^0.1.0"
}
}3. Clean up workspace config (optional)
Remove the package from pnpm-workspace.yaml:
packages:
- apps/*
# Remove: - ../react-native-keyboard-composer4. Reinstall dependencies
pnpm installQuick Toggle Scripts (Optional)
Add these scripts to your consuming app's package.json for easy switching:
{
"scripts": {
"use-local-keyboard": "pnpm pkg set dependencies.@launchhq/react-native-keyboard-composer=workspace:* && pnpm install",
"use-published-keyboard": "pnpm pkg set dependencies.@launchhq/react-native-keyboard-composer=^0.1.0 && pnpm install"
}
}Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
Support
If you find this library helpful, consider supporting its development:
License
MIT © LaunchHQ
