← Назад к шаблонам На главную
Формы

Форма отзывов с предпросмотром

Форма для сбора отзывов на сайте. Пользователь сразу видит, как будет выглядеть его отзыв, может выбрать оценку и настроение, а владелец сайта получает более аккуратные и понятные заявки.

Сложность: Сложная HTML CSS JavaScript live preview rating UI form validation micro animations
Отзывы клиентов

Отзыв сразу превращается в готовую карточку

Клиент пишет отзыв, добавляет фото результата и ставит оценку. Справа сразу видно, как карточка будет выглядеть на сайте.

0/220 символов

Фото результата и текст сразу обновляют предпросмотр.

Фото результата
А

Анна

Консультация

Очень понравилось, что всё объяснили спокойно и понятным языком. Теперь стало ясно, с чего начинать и какие шаги делать дальше.

★★★★★ Отличный опыт
<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);
🚀 Рассчитать проект