Формы
Форма отзывов с предпросмотром
Форма для сбора отзывов на сайте. Пользователь сразу видит, как будет выглядеть его отзыв, может выбрать оценку и настроение, а владелец сайта получает более аккуратные и понятные заявки.
Отзывы клиентов
Отзыв сразу превращается в готовую карточку
Клиент пишет отзыв, добавляет фото результата и ставит оценку. Справа сразу видно, как карточка будет выглядеть на сайте.
Фото результата
А
Анна
Консультация
Очень понравилось, что всё объяснили спокойно и понятным языком. Теперь стало ясно, с чего начинать и какие шаги делать дальше.
Отличный опыт
<section class="review-composer">
<div class="review-composer__intro">
<span class="review-composer__badge">Отзывы клиентов</span>
<h2>Отзыв сразу превращается в готовую карточку</h2>
<p>
Клиент пишет отзыв, добавляет фото результата и ставит оценку.
Справа сразу видно, как карточка будет выглядеть на сайте.
</p>
</div>
<div class="review-composer__workspace">
<form class="review-form">
<label>
Имя
<input class="review-name" type="text" placeholder="Например, Анна">
</label>
<label>
Фото результата
<input class="review-result-photo" type="file" accept="image/*">
</label>
<label>
Проект или услуга
<input class="review-service" type="text" placeholder="Стрижка / консультация / сайт">
</label>
<label>
Текст отзыва
<textarea class="review-text" rows="5" maxlength="220" placeholder="Что понравилось? Что получилось в итоге?"></textarea>
</label>
<div class="review-counter">
<span class="review-counter__value">0</span>/220 символов
</div>
<div class="review-rating" aria-label="Оценка">
<button type="button" data-rating="1">★</button>
<button type="button" data-rating="2">★</button>
<button type="button" data-rating="3">★</button>
<button type="button" data-rating="4">★</button>
<button type="button" data-rating="5">★</button>
</div>
<button class="review-submit" type="submit">
Подготовить отзыв
</button>
<p class="review-form__message">
Фото результата и текст сразу обновляют предпросмотр.
</p>
</form>
<article class="review-preview">
<div class="review-preview__image">
<span>Фото результата</span>
<img class="review-preview__photo" src="" alt="">
</div>
<div class="review-preview__content">
<div class="review-preview__top">
<div class="review-preview__avatar">А</div>
<div>
<h3 class="preview-name">Анна</h3>
<p class="preview-service">Консультация</p>
</div>
</div>
<p class="review-preview__text">
Очень понравилось, что всё объяснили спокойно и понятным языком.
Теперь стало ясно, с чего начинать и какие шаги делать дальше.
</p>
<div class="review-preview__bottom">
<span class="preview-rating">★★★★★</span>
<span class="preview-status">Отличный опыт</span>
</div>
</div>
</article>
</div>
</section>
.review-composer {
min-height: 640px;
padding: 46px;
border-radius: 28px;
overflow: hidden;
background:
radial-gradient(circle at 12% 18%, rgba(59, 130, 246, 0.30), transparent 32%),
radial-gradient(circle at 88% 82%, rgba(236, 72, 153, 0.22), transparent 34%),
linear-gradient(135deg, #020617, #0f172a);
}
.review-composer__intro {
max-width: 820px;
margin-bottom: 34px;
}
.review-composer__badge {
display: inline-flex;
margin-bottom: 16px;
padding: 8px 13px;
border-radius: 999px;
color: #bfdbfe;
background: rgba(59, 130, 246, 0.16);
border: 1px solid rgba(96, 165, 250, 0.45);
font-size: 13px;
}
.review-composer__intro h2 {
margin: 0 0 16px;
color: #ffffff;
font-size: clamp(32px, 5vw, 56px);
line-height: 1.02;
letter-spacing: -0.05em;
}
.review-composer__intro p {
color: #cbd5e1;
font-size: 17px;
line-height: 1.7;
}
.review-composer__workspace {
display: grid;
grid-template-columns: 1fr 0.95fr;
gap: 26px;
align-items: stretch;
}
.review-form,
.review-preview {
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 26px;
background: rgba(15, 23, 42, 0.78);
box-shadow: 0 28px 80px rgba(2, 6, 23, 0.5);
}
.review-form {
display: grid;
gap: 15px;
padding: 26px;
}
.review-form label {
display: grid;
gap: 8px;
color: #dbeafe;
font-size: 14px;
}
.review-form input,
.review-form textarea {
width: 100%;
border: 1px solid #334155;
border-radius: 16px;
padding: 13px 14px;
color: #ffffff;
background: #020617;
outline: none;
}
.review-form textarea {
resize: vertical;
}
.review-form input:focus,
.review-form textarea:focus {
border-color: #60a5fa;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.16);
}
.review-result-photo::file-selector-button {
margin-right: 12px;
border: 0;
border-radius: 999px;
padding: 8px 12px;
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #9333ea);
cursor: pointer;
}
.review-counter {
color: #94a3b8;
font-size: 13px;
}
.review-rating {
display: flex;
gap: 8px;
}
.review-rating button {
width: 42px;
height: 42px;
border: 1px solid #334155;
border-radius: 14px;
color: #64748b;
background: #020617;
cursor: pointer;
font-size: 20px;
transition: 0.2s;
}
.review-rating button.active {
color: #facc15;
border-color: rgba(250, 204, 21, 0.65);
background: rgba(250, 204, 21, 0.12);
transform: translateY(-2px);
}
.review-submit {
border: 0;
border-radius: 16px;
padding: 14px 18px;
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #9333ea);
cursor: pointer;
transition: 0.25s;
}
.review-submit:hover {
transform: translateY(-2px);
}
.review-form__message {
margin: 0;
color: #94a3b8;
font-size: 14px;
}
.review-preview {
overflow: hidden;
display: flex;
flex-direction: column;
}
.review-preview__image {
position: relative;
min-height: 260px;
display: grid;
place-items: center;
overflow: hidden;
color: #93c5fd;
background:
radial-gradient(circle at 30% 20%, rgba(59, 130, 246, 0.28), transparent 36%),
linear-gradient(135deg, #020617, #172554);
}
.review-preview__image span {
position: relative;
z-index: 1;
border: 1px solid rgba(96, 165, 250, 0.45);
border-radius: 999px;
padding: 8px 13px;
background: rgba(15, 23, 42, 0.72);
}
.review-preview__photo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: none;
}
.review-preview.has-photo .review-preview__photo {
display: block;
}
.review-preview.has-photo .review-preview__image span {
display: none;
}
.review-preview__content {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
padding: 30px;
}
.review-preview__content::before {
content: "";
position: absolute;
width: 220px;
height: 220px;
right: -90px;
bottom: -90px;
border-radius: 50%;
background: radial-gradient(circle, rgba(236, 72, 153, 0.22), transparent 70%);
}
.review-preview__top,
.review-preview__bottom,
.review-preview__text {
position: relative;
z-index: 2;
}
.review-preview__top {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
}
.review-preview__avatar {
width: 54px;
height: 54px;
display: grid;
place-items: center;
border-radius: 18px;
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #9333ea);
font-size: 23px;
font-weight: 800;
}
.preview-name {
margin: 0;
color: #ffffff;
font-size: 24px;
}
.preview-service {
margin: 4px 0 0;
color: #94a3b8;
}
.review-preview__text {
color: #dbeafe;
font-size: 19px;
line-height: 1.65;
}
.review-preview__bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-top: auto;
padding-top: 30px;
}
.preview-rating {
color: #facc15;
letter-spacing: 3px;
}
.preview-status {
border: 1px solid rgba(96, 165, 250, 0.45);
border-radius: 999px;
padding: 8px 13px;
color: #bfdbfe;
background: rgba(59, 130, 246, 0.16);
}
.review-preview.is-updated {
animation: reviewPulse 0.35s ease;
}
@keyframes reviewPulse {
from {
transform: scale(0.985);
border-color: rgba(96, 165, 250, 0.8);
}
to {
transform: scale(1);
border-color: rgba(148, 163, 184, 0.28);
}
}
@media (max-width: 768px) {
.review-composer {
padding: 26px 18px;
}
.review-composer__workspace {
grid-template-columns: 1fr;
}
.review-preview__image {
min-height: 220px;
}
.review-preview__text {
font-size: 17px;
}
.review-rating {
justify-content: space-between;
}
.review-rating button {
width: 38px;
height: 38px;
}
.review-preview__bottom {
align-items: flex-start;
flex-direction: column;
}
}
const reviewName = document.querySelector(".review-name");
const reviewResultPhoto = document.querySelector(".review-result-photo");
const reviewService = document.querySelector(".review-service");
const reviewText = document.querySelector(".review-text");
const reviewCounter = document.querySelector(".review-counter__value");
const previewName = document.querySelector(".preview-name");
const previewService = document.querySelector(".preview-service");
const previewText = document.querySelector(".review-preview__text");
const previewAvatar = document.querySelector(".review-preview__avatar");
const previewPhoto = document.querySelector(".review-preview__photo");
const previewRating = document.querySelector(".preview-rating");
const previewStatus = document.querySelector(".preview-status");
const previewCard = document.querySelector(".review-preview");
const ratingButtons = document.querySelectorAll(".review-rating button");
const reviewForm = document.querySelector(".review-form");
const reviewMessage = document.querySelector(".review-form__message");
const ratingLabels = {
1: "Разочарован",
2: "Есть замечания",
3: "Неплохо",
4: "Всё понравилось",
5: "Отличный опыт"
};
let currentRating = 5;
function pulsePreview() {
previewCard.classList.remove("is-updated");
void previewCard.offsetWidth;
previewCard.classList.add("is-updated");
}
function updatePreview() {
const name = reviewName.value.trim() || "Анна";
const service = reviewService.value.trim() || "Консультация";
const text = reviewText.value.trim() || "Очень понравилось, что всё объяснили спокойно и понятным языком. Теперь стало ясно, с чего начинать и какие шаги делать дальше.";
previewName.textContent = name;
previewService.textContent = service;
previewText.textContent = text;
previewAvatar.textContent = name.charAt(0).toUpperCase();
reviewCounter.textContent = reviewText.value.length;
pulsePreview();
}
function updateRating(rating) {
currentRating = rating;
ratingButtons.forEach(function (item) {
item.classList.toggle("active", Number(item.dataset.rating) <= currentRating);
});
previewRating.textContent = "★".repeat(currentRating) + "☆".repeat(5 - currentRating);
previewStatus.textContent = ratingLabels[currentRating];
pulsePreview();
}
reviewResultPhoto.addEventListener("change", function () {
const file = reviewResultPhoto.files[0];
if (!file) {
previewCard.classList.remove("has-photo");
previewPhoto.src = "";
pulsePreview();
return;
}
const reader = new FileReader();
reader.onload = function (event) {
previewPhoto.src = event.target.result;
previewCard.classList.add("has-photo");
pulsePreview();
};
reader.readAsDataURL(file);
});
[reviewName, reviewService, reviewText].forEach(function (field) {
field.addEventListener("input", updatePreview);
});
ratingButtons.forEach(function (button) {
button.addEventListener("click", function () {
updateRating(Number(button.dataset.rating));
});
});
reviewForm.addEventListener("submit", function (event) {
event.preventDefault();
const nameReady = reviewName.value.trim().length >= 2;
const textReady = reviewText.value.trim().length >= 20;
if (!nameReady || !textReady) {
reviewMessage.textContent = "Добавьте имя и отзыв хотя бы на 20 символов.";
reviewMessage.style.color = "#fca5a5";
return;
}
reviewMessage.textContent = "Отзыв готов. В реальном проекте здесь отправка на сервер или модерацию.";
reviewMessage.style.color = "#86efac";
});
updateRating(5);