背景介绍
正如我们所知道的 textarea
是一个行内块元素 display: inline-block
并且它的默认宽高由 cols
& rows
决定, 也就是说 textarea
的 height
并不会自适应于内容长度.
textarea 的宽高是如何决定的? 参考张鑫旭的文章 HTML textarea cols,rows属性和宽度高度关系研究
那么, 我们今天的任务就是来思考如何创建一个 高度内容自适应的 textarea 组件,我将介绍三种思路实现 高度内容自适应的 textarea,具体代码 textareaAutoSizeSolutions
方案概要
这是三种方案的概述和实现思路的简介, 实现方案 & 遇到的坑 & 拓展知识点, 点击查看 teeeemoji 的 demo.
方案一: 两次调整 textarea.style.height
textarea
的 onchange
触发 resize
方法,下面是 resize
方法的逻辑
textarea.style.height = 'auto';// 1. 让 textarea 的高度恢复默认
textarea.style.height = textarea.scrollHeight + 'px';// 2. textarea.scrollHeight 表示 *textarea* 内容的实际高度
方案二: 利用一个 ghostTextarea 获得输入框内容高度, 再将这个高度设置给真实的 textarea
textarea
构建时创建 ghostTextarea, onchange
触发 resize
方法:
- 创建 textarea 的时候, 同时创建一个一模一样的隐藏 ghostTextarea;
- ghostTextarea 的属性全部克隆自
textarea
, 但是 ghostTextarea 是 隐藏 的, 并且ghostTextarea.style.height = 0
; 也就是说 ghostTextarea.scrollHeight 就是textarea
中内容的真是高度。
resize 方法处理流程:
textarea.value
先设置给 ghostTextarea,- 拿到
ghostTextarea.scrollHeight
- 将
textarea.style.height = ghostTextarea.scrollHeight
方案三: 使用 (div | p | ...).contenteditable 代替 textarea 作为输入框
div
是块级元素, 高度本身就是内容自适应的(除非设置 max-width
or min-widht
) 使用 contenteditable
让 div
代替 textarea
, 省去各种计算高度的逻辑。
方案对比
满分3分, 三种方案通过优化, 在用户体验和兼容性上都能达到满分. 因此差别仅仅在于这几个方案的实现难度. (仅仅是基于 react 组件的实现复杂度). 方案对比:

毫无疑问方案一是最优选择, 多加1分以示奖励;
方案一两次调整 textarea.style.height
实现思路
- 渲染一个
textarea
元素
<textarea
ref={this.bindRef}
className={style['textarea'] + ' ' + className}
placeholder={placeholder}
value={value}
onChange={this.handleChange} // 看这里
/>
textarea
的onChange
事件触发resize
handleChange(e) {
this.props.onChange(e.target.value);
this.resize(); // 看这里
}
resize
事件的实现
// 重新计算 textarea 的高度
resize() {
if (this.inputRef) {
console.log('resizing...')
this.inputRef.style.height = 'auto';
this.inputRef.style.height = this.inputRef.scrollHeight + 'px';
}
}
- 注意
componentDidMount
的时候, 执行一次resize
方法, 初始化textarea
的高度哦.
优化点
避免两次渲染,造成内容抖动
在 react
中, 组件 receiveProps
的时候会 render
一次, 直接调整 textarea
的 height
也会浏览器的重绘,那么就会造成两次重绘, 并且两次重绘的时候, textarea
的内容可能会发生抖动.
优化思路:先触发 resize
后触发 render
用最简单的思路完美解决问题
方案二: 利用一个 ghostTextarea 获得输入框内容高度, 再将这个高度设置给真实的 textarea
实现思路
同时渲染两个 textarea
, 一个真实 textarea
一个隐藏 textarea
return (
<div className={style['comp-textarea-with-ghost']}>
<textarea // 这个是真的
ref={this.bindRef}
className={style['textarea'] + ' ' + className}
placeholder={placeholder}
value={value}
onChange={this.handleChange}
style={{height}}
/>
<textarea // 这个是 ghostTextarea
className={style['textarea-ghost']}
ref={this.bindGhostRef}
onChange={noop}
/>
</div>
)
初始化的时候拷贝属性,初始化必须使用工具方法将 textarea
的属性拷贝到 ghostTextarea
去. 因为 textarea
的样式再组件外也能控制, 因此初始化的时候 copy style 是最安全的。
这是所以要拷贝的属性的列表:
const SIZING_STYLE = [
'letter-spacing',
'line-height',
'font-family',
'font-weight',
'font-size',
'font-style',
'tab-size',
'text-rendering',
'text-transform',
'width',
'text-indent',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'box-sizing'
];
这是 ghostTextarea 的隐藏属性列表:
const HIDDEN_TEXTAREA_STYLE = {
'min-height': '0',
'max-height': 'none',
height: '0',
visibility: 'hidden',
overflow: 'hidden',
position: 'absolute',
'z-index': '-1000',
top: '0',
right: '0',
};
这是拷贝 style
的工具方法
// 拿到真实 textarea 的所有 style
function calculateNodeStyling(node) {
const style = window.getComputedStyle(node);
if (style === null) {
return null;
}
return SIZING_STYLE.reduce((obj, name) => {
obj[name] = style.getPropertyValue(name);
return obj;
}, {});
}
// 拷贝 真实 textarea 的 style 到 ghostTextarea
export const copyStyle = function (toNode, fromNode) {
const nodeStyling = calculateNodeStyling(fromNode);
if (nodeStyling === null) {
return null;
}
Object.keys(nodeStyling).forEach(key => {
toNode.style[key] = nodeStyling[key];
});
Object.keys(HIDDEN_TEXTAREA_STYLE).forEach(key => {
toNode.style.setProperty(
key,
HIDDEN_TEXTAREA_STYLE[key],
'important',
);
});
}
textarea 的 onChange
事件 先 reize 再触发 change 事件
handleChange(e) {
this.resize();
let value = e.target.value;
this.props.onChange(value);
}
textarea 的 resize 方法
resize() {
console.log('resizing...')
const height = calculateGhostTextareaHeight(this.ghostRef, this.inputRef);
this.setState({height});
}
calculateGhostTextareaHeight 工具方法
// 先将内容设置进 ghostTextarea, 再拿到 ghostTextarea.scrollHeight
export const calculateGhostTextareaHeight = function (ghostTextarea, textarea) {
if (!ghostTextarea) {
return;
}
ghostTextarea.value = textarea.value || textarea.placeholder || 'x'
return ghostTextarea.scrollHeight;
}
优化点
避免两次渲染,造成内容抖动
在 react
中, 组件 receiveProps
的时候会 render
一次, 给 textarea
设置 height
属性也会浏览器的重绘.那么就会造成两次重绘, 并且两次重绘的时候, textarea
的内容可能会发生抖动.
下面两种思路, 在 demo 中均有体现
优化思路一: 合并祯渲染
使用 window.requestAnimationFrame
& window.cancelAnimationFrame
来取消第一祯的渲染, 而直接渲染高度已经调整好的 textarea
;
优化思路二: 减少渲染次数
利用 react
批处理 setState
方法, 减少 rerender
的特性; 在 textarea onChange
方法中同时触发两个 setState
;
更多优化思路
- 页面存在多个
textarea
的时候, 能不能考虑 复用同一个ghostTextarea
方案三: 使用 div.contenteditable 代替 textarea
实现思路
渲染一个 div.contenteditable=true
return (
<div className={style['comp-div-contenteditable']}>
<div
ref={this.bindRef}
className={classname(style['textarea'], className, {[style['empty']]: !value})}
onChange={this.handleChange}
onPaste={this.handlePaste}
placeholder={placeholder}
contentEditable
/>
</div>
)
获取 & 设置 编辑的内容: textarea
通过 textarea.value
来取值 or 设置值, 但换成了 div
之后, 就要使用 div.innerHTML
or div.innerText
来取值 or 设置值.
使用 div.innerHTML
会出现以下两种问题:
&
会被转码成&
- 空白符合并 使用
div.innerText
在低版本 firfox 上要做兼容处理.
因此使用哪种方式 主要看需求.
placeholder 的实现:
div
的 placeholder
属性是无效, 不会显示出来的, 现存一种最简单的方式, 使用纯 css
的方式实现 div
的 placeholder
.textarea[placeholder]:empty:before { /*empty & before 两个伪类*/
content: attr(placeholder); /*attr 函数*/
color: #555;
}
优化点
去除支持富文本
div.contenteditable
是默认支持富文本的, 可能会以 粘贴 or 拖拽 让输入框出现富文本;

监听 div 的 onPaste 事件
handlePaste(e) {
e.preventDefault();
let text = e.clipboardData.getData('text/plain'); // 拿到纯文本
document.execCommand('insertText', false, text); // 让浏览器执行插入文本操作
}
几个大网站的高度自适应 textarea 对比
我分别查看了微博, ant.design组件库, 知乎 的自适应输入框的实现.
微博: 采用方案二
未输入时

输入后

但是微博的实现存在用户体验上的缺陷, 会抖动!!!

ant.design: 采用方案二
体验超级棒哦

知乎: 采用方案三
看上去竟然存在 bug , 其实上面的截图也有

参考链接列表
- textarea mdn 文档: 看看有哪些影响 textarea 宽高的属性
- HTML textarea cols,rows属性和宽度高度关系研究
- CSS Tricks: textarea 使用上的小技巧. 一些样式技巧.
- 开源的 react auto resize textarea: 一个更好的方案二的实现, 源码优雅短小
- can i use 兼容性检查工具: execCommand, innerText, requireAnimationFrame, 等等各种各样属性的兼容性检查
- contenteditable MDN
- 一个方案三的实现, a good demo
- 小tip: 如何让contenteditable元素只能输入纯文本