Как создать форму ввода как в ChatGPT в Next.js 16 + Tailwind v4
Полное руководство по реализации формы ввода как в ChatGPT в Next.js 16 + Tailwind v4. Узнайте как избежать бесконечного переключения состояний и создать плавную анимацию.
Как реализовать форму ввода как в веб-версии ChatGPT в Next.js 16 + Tailwind v4?
Я разрабатываю проект на Next.js 16 + Tailwind v4, который является оберткой над LLM. Мне нужно реализовать форму ввода, аналогичную той, что используется в веб-версии ChatGPT.
Моя текущая реализация использует подход с CSS Grid и двумя состояниями формы:
-
Сжатое состояние (isExpanded === false):
- 1 ряд, 3 колонки
- Структура: кнопка | textarea | контейнер с двумя кнопками
-
Расширенное состояние (isExpanded === true):
- 2 ряда, 3 колонки
- Структура: textarea | textarea | textarea
- Второй ряд: кнопка | пусто | контейнер с двумя кнопками
Проблема: при автоматическом переключении состояния isExpanded на основе высоты textarea возникает бесконечное переключение между состояниями, так как изменение сетки влияет на ширину textarea.
Я пробовал:
- Фиксировать isExpanded в true при первом изменении высоты
- Анимировать изменения с помощью чистого CSS (неудачно - элементы теряли форму)
- Использовать Framer Motion для анимации лейаута (лучше, но все равно неказисто)
Вопросы:
- Как правильно реализовать такую форму ввода с плавной анимацией?
- Стоит ли использовать textarea или contenteditable div для поля ввода?
- Как избежать проблемы с бесконечным переключением состояний?
- Какие есть лучшие практики для реализации таких UI-компонентов?
Для реализации формы ввода как в веб-версии ChatGPT в Next.js 16 + Tailwind v4 используйте комбинацию CSS Grid с auto-growing textarea и правильным управлением состоянием. Ключ к успеху — разделение логики высоты от логики ширины, чтобы избежать бесконечного переключения состояний, и использование современных техник анимации.
Содержание
- Основные подходы к реализации
- Автоматически расширяющийся textarea
- Сравнение textarea и contenteditable div
- Решение проблемы бесконечного переключения состояний
- Практическая реализация в Next.js + Tailwind
- Анимация и переходы
- Лучшие практики и рекомендации
Основные подходы к реализации
Существует несколько подходов к созданию формы ввода типа ChatGPT, каждый со своими преимуществами и недостатками:
- CSS Grid с двумя состояниями — ваш текущий подход, который требует тщательного управления состоянием
- Flexbox с динамической высотой — более простой подход с автоматическим изменением размера
- Contenteditable div — позволяет richer функциональность, но сложнее в реализации
- Комбинированный подход — использование библиотек или готовых компонентов
CSS-Tricks предлагает элегантное решение с использованием CSS Grid, где textarea автоматически расширяется в зависимости от содержимого, без необходимости переключения состояний.
Автоматически расширяющийся textarea
Наиболее эффективным решением является использование техники с CSS Grid, где textarea расширяется автоматически без переключения состояний. Вот пример реализации:
<div class="grid text-sm after:px-3.5 after:py-2.5
[&>textarea]:text-inherit after:text-inherit
[&>textarea]:resize-none [&>textarea]:overflow-hidden
[&>textarea]:[grid-area:1/1/2/2]
after:[grid-area:1/1/2/2]
after:whitespace-pre-wrap after:invisible
after:content-[attr(data-cloned-val)' ']
after:border">
<textarea
class="w-full text-slate-600 bg-slate-100 border border-transparent
hover:border-slate-200 appearance-none rounded px-3.5 py-2.5
outline-none focus:bg-white focus:border-indigo-400
focus:ring-2 focus:ring-indigo-100"
name="message"
id="message"
rows="2"
onInput="this.parentNode.dataset.clonedVal = this.value"
placeholder="Your request..."
required>
</textarea>
</div>
Этот подход из Cruip использует псевдоэлемент after для измерения высоты контента без необходимости переключения состояний.
Сравнение textarea и contenteditable div
Преимущества textarea:
- Безопасность: Автоматически экранирует HTML теги
- Простота: Легче в управлении состоянием
- Семантика: Правильно обрабатывается формами
- Доступность: Лучше поддержка скринридерами
Преимущества contenteditable div:
- Форматирование: Поддержка богатого текста
- Гибкость: Можно встраивать изображения и другие элементы
- Стилизация: Больше возможностей для кастомизации
Как отмечает Stack Overflow, “используйте contenteditable div только тогда, когда вам нужна возможность форматировать текст”. Для простого текстового ввода textarea предпочтительнее.
В Reddit также подчеркивают, что contenteditable — это плохая практика для простых текстовых полей, так как он принимает HTML от пользователя, что небезопасно.
Решение проблемы бесконечного переключения состояний
Основная проблема вашего подхода — взаимозависимость высоты и ширины при переключении состояний. Вот несколько решений:
1. Разделение логики высоты и ширины
const [isExpanded, setIsExpanded] = useState(false);
const [text, setText] = useState('');
const textareaRef = useRef(null);
const handleInputChange = (e) => {
const newText = e.target.value;
setText(newText);
// Проверяем высоту только после обновления значения
if (textareaRef.current) {
const scrollHeight = textareaRef.current.scrollHeight;
const clientHeight = textareaRef.current.clientHeight;
if (scrollHeight > clientHeight && !isExpanded) {
setIsExpanded(true);
}
}
};
// Сбрасываем состояние при очистке поля
useEffect(() => {
if (text.trim() === '' && isExpanded) {
setIsExpanded(false);
}
}, [text, isExpanded]);
2. Использование CSS transitions вместо анимации состояний
.chat-input-container {
transition: all 0.3s ease;
}
.chat-input {
transition: height 0.3s ease;
}
3. Фиксированная ширина в обоих состояниях
Вместо изменения структуры сетки, изменяйте только высоту textarea, сохраняя фиксированную ширину:
<div className="flex items-end space-x-2">
<button className="p-2">📎</button>
<div className="flex-1">
<textarea
ref={textareaRef}
className={`w-full resize-none overflow-hidden transition-all duration-300 ${
isExpanded ? 'min-h-[200px]' : 'min-h-[60px]'
}`}
value={text}
onChange={handleInputChange}
placeholder="Ask anything..."
rows={1}
/>
</div>
<div className="flex items-end space-x-1">
<button className="p-2">🎲</button>
<button className="p-2">➡️</button>
</div>
</div>
Практическая реализация в Next.js + Tailwind
Вот полная реализация компонента формы ввода:
'use client';
import { useState, useRef, useEffect } from 'react';
export default function ChatInput() {
const [isExpanded, setIsExpanded] = useState(false);
const [text, setText] = useState('');
const textareaRef = useRef(null);
const handleInputChange = (e) => {
const newText = e.target.value;
setText(newText);
if (textareaRef.current) {
const scrollHeight = textareaRef.current.scrollHeight;
const clientHeight = textareaRef.current.clientHeight;
if (scrollHeight > clientHeight && !isExpanded) {
setIsExpanded(true);
}
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
// Обработка отправки сообщения
console.log('Sending:', text);
setText('');
setIsExpanded(false);
}
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, [text]);
return (
<div className="border-t border-gray-200 p-4 bg-white">
<div className="flex items-end space-x-2 max-w-4xl mx-auto">
{/* Кнопка добавления файла */}
<button className="p-2 text-gray-500 hover:text-gray-700 transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
</button>
{/* Контейнер для textarea */}
<div className="flex-1">
<textarea
ref={textareaRef}
className={`w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
transition-all duration-200 resize-none overflow-hidden
${isExpanded ? 'min-h-[200px] max-h-[400px]' : 'min-h-[60px]'}`}
value={text}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Ask anything..."
rows={1}
/>
</div>
{/* Кнопки действий */}
<div className="flex items-end space-x-1">
<button className="p-2 text-gray-500 hover:text-gray-700 transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button
className={`p-2 rounded-lg transition-colors ${
text.trim()
? 'bg-blue-500 hover:bg-blue-600 text-white'
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
disabled={!text.trim()}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
);
}
Анимация и переходы
Для плавных анимаций используйте CSS transitions и Tailwind utilities:
/* В вашем globals.css или Tailwind config */
.chat-input-textarea {
transition: height 0.2s ease-out, padding 0.2s ease-out;
}
.chat-input-button {
transition: all 0.2s ease;
}
Или с помощью Tailwind:
<textarea
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
transition-all duration-200 ease-out resize-none overflow-hidden"
// ... другие props
/>
Для более сложных анимаций рассмотрите Framer Motion:
import { motion, AnimatePresence } from 'framer-motion';
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className="flex items-end space-x-2"
>
{/* ваш контент */}
</motion.div>
Лучшие практики и рекомендации
-
Используйте useRef для прямого доступа к DOM элементам — это обеспечивает лучшую производительность, чем поиск элементов через querySelector.
-
Оптимизируйте обработчики событий — используйте useCallback для обработчиков, которые передаются как пропсы.
-
Обрабатывайте edge cases — пустые строки, очень длинные тексты, специальные символы.
-
Добавьте валидацию — проверяйте минимальную и максимальную длину текста.
-
Реализуйте доступность — добавьте соответствующие aria-атрибуты и обработку клавиатурных сокращений.
-
Тестируйте на разных устройствах — убедитесь, что форма работает корректно на мобильных устройствах.
-
Используйте TypeScript — для лучшей типизации и IntelliSense.
-
Оптимизируйте производительность — используйте React.memo и useMemo где это уместно.
Как отмечено в GeeksforGeeks, правильная структура компонента и управление состоянием — ключ к созданию отзывчивых и эффективных форм в Next.js.
Источники
- Auto-Growing Textarea with Tailwind CSS - Cruip
- Auto-Growing Inputs & Textareas | CSS-Tricks
- What are the cons of using a contentEditable div rather than a textarea? - Stack Overflow
- What should I use instead of contenteditable? - Reddit
- Create Form Layouts UI using Next.JS and Tailwind CSS - GeeksforGeeks
- GitHub - Next.js TailwindCSS ChatGPT Clone
- ChatGPT Chatbot Using Next.js + Tailwind CSS - Medium
Заключение
Для реализации формы ввода как в ChatGPT в Next.js + Tailwind v4:
-
Предпочитайте textarea над contenteditable для простых текстовых полей — это безопаснее и проще в управлении.
-
Используйте CSS Grid + псевдоэлементы для автоматического изменения размера textarea без переключения состояний.
-
Разделяйте логику высоты и ширины — изменяйте только высоту textarea, сохраняя фиксированную ширину контейнера.
-
Добавьте плавные переходы с помощью CSS transitions или Framer Motion для лучшего пользовательского опыта.
-
Обрабатывайте edge cases — пустые строки, очень длинные тексты, и специальные символы.
-
Оптимизируйте производительность — используйте useRef и useCallback для эффективной работы с DOM и событиями.
Следуя этим практикам, вы создадите надежный и красивый компонент формы ввода, который будет работать безупречно в вашем Next.js приложении.