1. 为什么我们需要高度自适应的输入框?
不知道你有没有遇到过这种情况:在写一个评论框,或者一个笔记应用的编辑区,用户噼里啪啦打了一大段字,结果输入框纹丝不动,文字全都挤在了一行里,要么就是出现了丑陋的滚动条,用户体验非常糟糕。这就是典型的固定高度输入框带来的问题。
传统的 <input> 标签是单行输入框,它的高度在渲染时就被固定了,你打再多字,它也不会“长高”。而 <textarea> 虽然默认是多行的,但它的高度也是固定的,除非用户手动去拖拽右下角调整大小。但在很多现代应用场景里,我们希望输入框能像聊天气泡或者便签纸一样,随着用户输入的内容自动调整高度,内容少的时候小巧精致,内容多的时候又能从容展开,视觉上非常流畅。
这种“高度自适应”的需求,在移动端尤其强烈。想想看,你在手机上发表一条动态,输入框如果一开始就占了大半屏,会显得很空;但如果只给一行的高度,写长文时又要不停滚动,体验就割裂了。一个能“呼吸”的输入框,能让用户感觉应用更智能、更贴心。
所以,实现一个高度自适应的多行输入框,并不是炫技,而是实打实地提升产品交互细节。接下来,我就带你深入对比几种主流实现方案,从最推荐的到最折腾的,结合我踩过的坑和实战代码,帮你找到最适合你项目的那一个。
2. 方案一:使用 <textarea> + JavaScript(最推荐、最稳妥)
这是目前实现高度自适应输入框的“黄金标准”,也是我个人在绝大多数项目中的首选方案。它的核心思路非常简单:利用 <textarea> 原生支持多行文本的特性,然后通过 JavaScript 监听输入事件,动态调整它的高度。
2.1 核心实现原理与代码
原理就一句话:让 <textarea> 的高度始终等于其内容的真实高度(scrollHeight)。scrollHeight 是一个只读属性,它代表了元素在不使用滚动条的情况下,为了适应所有内容所需要的最小高度。
我们通过监听 input 事件(即用户输入时)来触发高度调整。这里有个关键细节:在设置新高度前,需要先将高度重置为 auto 或者一个很小的基础值(比如 1px),这是为了清除上一次计算的影响,让 scrollHeight 能重新计算当前内容的准确高度。
下面是一个最基础、无框架的 JavaScript 实现:
<textarea id="autoResizeTextarea" placeholder="说点什么吧..."></textarea>
<script>
const textarea = document.getElementById('autoResizeTextarea');
textarea.addEventListener('input', function () {
// 1. 先重置高度,强制重新计算 scrollHeight
this.style.height = 'auto';
// 2. 将高度设置为内容撑开的高度
this.style.height = this.scrollHeight + 'px';
});
// 可选:初始化时设置一次高度,以正确显示初始内容(如果有的话)
textarea.style.height = textarea.scrollHeight + 'px';
</script>
<style>
#autoResizeTextarea {
width: 300px; /* 固定宽度 */
min-height: 40px; /* 设置一个最小高度,看起来更舒服 */
padding: 10px;
font-size: 16px;
line-height: 1.5;
border: 1px solid #ddd;
border-radius: 8px;
resize: none; /* 关键!禁止用户手动拖拽调整大小,把控制权完全交给脚本 */
overflow-y: hidden; /* 隐藏垂直滚动条 */
box-sizing: border-box; /* 确保 padding 和 border 被计入宽度和高度计算 */
}
</style>
实测下来,这段代码在绝大多数现代浏览器中都非常“稳”。box-sizing: border-box; 这个 CSS 属性至关重要,它能确保我们计算的高度是包含内边距和边框的,避免出现高度计算误差导致文字被遮挡。
2.2 在 Vue 和 React 中的优雅实现
在实际的框架项目中,我们可以把逻辑封装成可复用的组件或 Hook,让代码更清晰。
Vue 3 (Composition API) 实现:
<template>
<textarea
ref="textareaRef"
v-model="text"
class="auto-resize-textarea"
placeholder="请输入内容"
@input="handleResize"
/>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue';
const text = ref('');
const textareaRef = ref(null);
const handleResize = () => {
const textarea = textareaRef.value;
if (!textarea) return;
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
});
};
// 组件挂载后,如果初始有值,也需要调整一次高度
onMounted(() => {
if (textareaRef.value) {
handleResize();
}
});
</script>
<style scoped>
.auto-resize-textarea {
width: 100%;
min-height: 44px;
padding: 12px;
line-height: 1.4;
border: 1px solid #ccc;
border-radius: 6px;
resize: none;
overflow-y: hidden;
box-sizing: border-box;
font-family: inherit;
}
</style>
React Hook 实现:
import React, { useState, useRef, useEffect } from 'react';
const useAutoResizeTextarea = (initialValue = '') => {
const [value, setValue] = useState(initialValue);
const textareaRef = useRef(null);
const resizeTextarea = () => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
};
useEffect(() => {
resizeTextarea();
}, [value]); // 当 value 变化时调整高度
const handleChange = (event) => {
setValue(event.target.value);
// 注意:这里不需要在 handleChange 中调用 resizeTextarea,
// 因为 useEffect 会监听 value 的变化并执行。
};
retu

106

被折叠的 条评论
为什么被折叠?



