简介:这是一份面向前端初学者与HTML/CSS练习者的网页仿写实战资源,聚焦问卷星个人版核心界面的静态实现,帮助开发者掌握结构语义化、响应式布局及交互元素样式设计。资源共4个文件,包含1个主入口HTML文件(组织页面骨架与表单结构)、1个CSS文件(实现导航栏、题型区域、按钮组等视觉样式与媒体查询适配)、2个JS文件(提供基础表单验证与动态交互支持),整体压缩包仅53KB,轻量易读,便于逐行分析与二次修改。已有854人学习下载,适合用于课堂实训、自学复盘或求职作品集搭建。读者可直接运行index.html查看完整效果,深入理解header/nav/section等语义标签应用,掌握盒模型、定位机制与移动端适配技巧,并基于现有结构快速拓展题目添加、逻辑跳转等进阶功能。
1. 为什么一个「模仿问卷星个人版」的 HTML+CSS 项目,比你想象中更值得花两小时亲手写一遍?
不是为了造轮子,而是为了把「表单交互」这个前端最基础、却最容易被框架黑盒化的模块,重新拉回肉眼可见的 DOM 层面。我带过三届实习生,发现一个共性:能熟练用 Vue 的v-model绑定表单、用 Element Plus 快速搭出问卷页面的人,一旦被要求在纯 HTML+CSS 环境下实现「单选题实时禁用其他选项」「多选题限制最多选3项」「必填项未填时点击提交按钮高亮提示」——立刻卡在:valid/:invalid伪类不会联动、<fieldset>的disabled传播逻辑不清晰、甚至搞不清<label>的for和id怎么配对才让点击文字能聚焦到对应输入框。
这个「模仿问卷星个人版 HTML+CSS」项目,本质是一份可运行的表单语义化教科书:它强制你手写<form>结构、手动处理required、minlength、pattern等原生校验属性,用 CSS 控制:focus-within、:has()(需现代浏览器)、[aria-invalid="true"]的视觉反馈,而不是依赖@change或@blur去 patch 一堆 JS 逻辑。它不追求功能完整(比如不实现数据提交后端),但每一道题的 DOM 结构、label 关联、校验触发时机、错误提示定位,都严格对标问卷星个人版网页源码中实际使用的最小可行模式。适合刚学完 HTML 表单基础、正准备啃 Vue 表单组件源码的前端新人;也适合想给老系统补一份轻量级问卷嵌入能力、拒绝引入整套 UI 框架的运维/测试工程师。你不需要懂 JavaScript,但必须愿意一行行敲<input type="radio">并理解它和<input type="checkbox">在 DOM 树中的父子关系差异。
2. 从零搭建结构:用语义化 HTML 构建可访问、可校验的问卷骨架
2.1 用<form>包裹全局,但绝不滥用action和method
问卷星个人版的问卷页实际是 SPA,但它的 HTML 骨架仍严格遵循表单语义。我们复现时,第一行就写死<!doctype html>,第二行<html lang="zh-cn">——这不是仪式感,而是让屏幕阅读器正确播报中文语境,同时避免 Chrome 在某些旧版本中因缺失lang导致字体 fallback 异常(尤其含中文标点时)。<head>中必须包含:
<meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>我的问卷 - 问卷星个人版风格</title>提示:
charset="utf-8"是硬性要求,漏掉会导致中文乱码;viewport不加则移动端页面会缩成一团,这是新手最常翻车的第一步。
<form>标签本身不设action(因为不真提交)和method(默认get,但这里无意义),但必须加novalidate属性:
<form id="survey-form" novalidate> <!-- 问题区块 --> </form>novalidate的作用是关闭浏览器原生弹窗式校验提示(那个难看的黄色气泡),把校验控制权完全交给 CSS 和后续可能的手动 JS。这是问卷星实际做法——它用自定义 tooltip 替代原生提示,体验更统一。
2.2 单选题:用<fieldset>+<legend>实现语义分组与禁用联动
问卷星的单选题结构不是简单堆<input>,而是用<fieldset>包裹一组同名name的 radio,并用<legend>定义题干。这样做的好处是:当fieldset设disabled时,所有内部 radio 自动禁用,且屏幕阅读器会把题干和选项作为整体播报。
<fieldset class="question-group" aria-labelledby="q1-label"> <legend id="q1-label" class="question-title">您最常使用的编程语言是?</legend> <div class="option-item"> <input type="radio" id="q1-opt1" name="q1" value="python" required> <label for="q1-opt1">Python</label> </div> <div class="option-item"> <input type="radio" id="q1-opt2" name="q1" value="javascript" required> <label for="q1-opt2">JavaScript</label> </div> <div class="option-item"> <input type="radio" id="q1-opt3" name="q1" value="rust" required> <label for="q1-opt3">Rust</label> </div> </fieldset>关键细节:
aria-labelledby="q1-label"让 screen reader 把legend文本作为该 fieldset 的标签读出;required加在每个 radio 上(而非 fieldset),是因为 HTML 规范要求至少一个 radio 被选中才算 valid,required放在任一 radio 即可触发校验;name="q1"是同一题所有选项的唯一标识,必须一致;id和for的配对必须精确(大小写、连字符都不能错),否则点击 label 不会聚焦 input。
2.3 多选题:用:checked+:nth-of-type()实现「最多选3项」的视觉约束
问卷星多选题的「已选3项后其他选项变灰不可点」效果,纯 CSS 可实现(无需 JS)。核心思路是:当第4个 checkbox 被选中时,用:has()选择器反向影响前面所有未选中的选项。但:has()兼容性有限(Chrome 105+ / Safari 15.4+),所以我们要提供降级方案——用:checked配合相邻兄弟选择器~,结合:nth-of-type()定位。
<fieldset class="question-group" aria-labelledby="q2-label"> <legend id="q2-label" class="question-title">您希望学习哪些前端技术?(最多选3项)</legend> <div class="option-item"> <input type="checkbox" id="q2-opt1" name="q2" value="react"> <label for="q2-opt1">React</label> </div> <div class="option-item"> <input type="checkbox" id="q2-opt2" name="q2" value="vue"> <label for="q2-opt2">Vue</label> </div> <div class="option-item"> <input type="checkbox" id="q2-opt3" name="q2" value="svelte"> <label for="q2-opt3">Svelte</label> </div> <div class="option-item"> <input type="checkbox" id="q2-opt4" name="q2" value="solid"> <label for="q2-opt4">SolidJS</label> </div> </fieldset>对应 CSS(兼容性方案):
/* 当任意3个被选中时,第4个及之后的未选中项禁用 */ input[type="checkbox"]:checked ~ input[type="checkbox"]:not(:checked) { opacity: 0.4; cursor: not-allowed; } /* 更精确的控制:只影响同一 fieldset 内的兄弟 */ .question-group input[type="checkbox"]:checked ~ .option-item input[type="checkbox"]:not(:checked) { opacity: 0.4; } /* 但此方案无法阻止用户点击第4个——需 JS 辅助,见第4章 */注意:纯 CSS 无法真正disable第4个 checkbox(pointer-events: none会破坏可访问性),所以视觉灰度只是提示,最终限制靠 JS。但这个 CSS 已足够提升用户预期,减少误操作。
3. 样式落地:用现代 CSS 实现问卷星级的响应式布局与交互动效
3.1 用display: grid构建题干与选项的垂直流,而非浮动或 inline-block
问卷星的题干左对齐、选项横向排列(小屏转竖排),传统做法是float: left或inline-block,但易产生间隙、换行错乱。现代解法是grid:
.question-group { display: grid; grid-template-rows: auto 1fr; gap: 12px; margin-bottom: 24px; } .question-title { font-size: 16px; font-weight: 600; color: #333; margin: 0; } .option-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }关键参数说明:
grid-template-rows: auto 1fr:题干占自然高度,选项区域占剩余空间(为未来加动画留余地);auto-fit+minmax(120px, 1fr):小屏时每项独占一行(minmax下限 120px 触发换行),大屏自动均分;gap统一控制内外间距,比margin更可控。
注意:
grid在 IE11 中不支持,但问卷星官网已放弃 IE 支持,我们复现时也无需兼容。若必须支持 IE,改用flex+flex-wrap,但需额外处理align-items垂直对齐。
3.2 用:focus-within实现「题干高亮」,替代 JS 监听 focus 事件
当用户点击任一选项时,整个题干区域应轻微上浮并加阴影,增强操作反馈。用 JS 监听每个 input 的focus太重,而:focus-within是完美解:
.question-group:focus-within { background-color: #f9f9f9; border-radius: 8px; padding: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.05); transition: all 0.2s ease; }原理:只要.question-group内任一可聚焦元素(如 input、button)获得焦点,该选择器即生效。它天然支持键盘 Tab 导航,且无需任何 JS 绑定。
3.3 用@media (prefers-reduced-motion)降低动画强度,满足无障碍要求
问卷星的 hover 动画很克制,我们复现时必须考虑「减少动画偏好」用户。CSS 提供原生支持:
@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }这行代码会让所有动画瞬间完成,避免眩晕风险。它是 WCAG 2.1 AA 级别要求,也是问卷星实际采用的策略。
4. 避坑指南:那些让你调试半小时才发现的 HTML+CSS 表单陷阱
4.1 现象:点击 label 无法聚焦到对应 input,原因:for和id大小写/空格不一致
原因:HTML 中id和for的匹配是严格字符串相等,q1-opt1和Q1-opt1被视为不同值;q1 opt1(含空格)会导致for解析失败。
解决:统一用 kebab-case(全小写+短横线),写完立即用浏览器开发者工具检查 Elements 面板中label[for]是否精准指向input[id]。
4.2 现象:required校验不触发,提交按钮无反应
原因:<form>缺少novalidate时,浏览器原生校验会拦截 submit 事件并弹出气泡;但若你写了onsubmit="return false"却没阻止默认行为,校验反而失效。
解决:要么删掉novalidate并接受原生气泡(不推荐),要么在 JS 中显式调用event.preventDefault(),并在form.checkValidity()返回 false 时手动添加aria-invalid="true"到对应 input。
4.3 现象:多选题「最多选3项」限制失效,用户仍可勾选第4项
原因:纯 CSS 的opacity: 0.4只是视觉提示,pointer-events: none会破坏键盘导航(Tab 键无法跳过该选项),违反可访问性。
解决:必须用 JS 监听change事件,当已选数量 ≥3 时,遍历所有未选中 checkbox 并设disabled = true;取消某一项时,再批量disabled = false。代码见第5章。
4.4 现象:移动端点击 radio/checkbox 无响应,尤其 iOS Safari
原因:iOS Safari 对<label>的点击区域有最小尺寸要求(44×44px),若 label 内只有文字且未设padding,点击会失效。
解决:给所有label添加display: block和padding: 10px 12px,或直接用::before伪元素生成更大点击区:
.option-item label::before { content: ""; display: inline-block; width: 20px; height: 20px; margin-right: 8px; vertical-align: middle; }4.5 现象:<fieldset>在部分 Android 浏览器中边框异常粗重
原因:Android WebView 默认给fieldset渲染 2px 粗边框,且border: none无效。
解决:重置fieldset样式:
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; /* 修复 Chrome 中的宽度撑开 */ }5. 交互动效增强:用最少 JS 实现「实时校验」与「提交反馈」
5.1 用checkValidity()+reportValidity()实现提交前校验
纯 HTMLrequired只在 submit 时触发,但问卷星在用户离开题目前就提示错误。我们用blur事件 +checkValidity()模拟:
document.querySelectorAll('input[required]').forEach(input => { input.addEventListener('blur', function() { if (!this.checkValidity()) { this.setAttribute('aria-invalid', 'true'); // 找到最近的 .option-item 或 .question-group 添加错误样式 this.closest('.option-item')?.classList.add('error'); } else { this.removeAttribute('aria-invalid'); this.closest('.option-item')?.classList.remove('error'); } }); });checkValidity()返回布尔值,reportValidity()会触发原生提示(我们不用),但aria-invalid是给屏幕阅读器和 CSSinput[aria-invalid="true"]用的关键钩子。
5.2 多选题「最多选3项」的 JS 实现:监听 change,动态 toggle disabled
document.querySelectorAll('fieldset[data-max-checks]').forEach(fieldset => { const max = parseInt(fieldset.dataset.maxChecks, 10); const checkboxes = fieldset.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(box => { box.addEventListener('change', () => { const checkedCount = fieldset.querySelectorAll('input[type="checkbox"]:checked').length; if (checkedCount >= max) { // 禁用所有未选中的 checkbox checkboxes.forEach(cb => { if (!cb.checked) cb.disabled = true; }); } else { // 启用所有 checkbox checkboxes.forEach(cb => cb.disabled = false); } }); }); });HTML 中加><button type="submit" id="submit-btn" aria-busy="false"> 提交问卷 </button>
JS 中:
const form = document.getElementById('survey-form'); const submitBtn = document.getElementById('submit-btn'); form.addEventListener('submit', async function(e) { e.preventDefault(); if (!form.checkValidity()) return; submitBtn.setAttribute('aria-busy', 'true'); submitBtn.disabled = true; submitBtn.textContent = '提交中…'; // 模拟 API 请求 await new Promise(r => setTimeout(r, 1200)); submitBtn.setAttribute('aria-busy', 'false'); submitBtn.disabled = false; submitBtn.textContent = '提交成功!'; // 3秒后恢复 setTimeout(() => { submitBtn.textContent = '提交问卷'; }, 3000); });CSS 响应aria-busy:
button[aria-busy="true"] { opacity: 0.7; cursor: wait; }血泪经验:不要用
button:disabled做加载态样式,因为disabled会禁用所有交互(包括aria-busy的语义),且无法用:hover等伪类。aria-busy是专为此场景设计的 ARIA 状态。
6. 进阶技巧:如何让这份 HTML+CSS 问卷真正「可嵌入」到任意页面?
6.1 用 CSS 自定义属性(Custom Properties)实现主题切换
问卷星支持浅色/深色主题,我们用 CSS 变量实现:
:root { --primary-color: #007bff; --text-color: #333; --bg-color: #fff; --border-color: #e0e0e0; } @media (prefers-color-scheme: dark) { :root { --primary-color: #0056b3; --text-color: #f0f0f0; --bg-color: #1a1a1a; --border-color: #333; } } body { color: var(--text-color); background-color: var(--bg-color); } .question-title { color: var(--primary-color); } input[type="radio"]:checked + label::before, input[type="checkbox"]:checked + label::before { background-color: var(--primary-color); }变量名全部以--开头,可在 JS 中动态修改:document.documentElement.style.setProperty('--primary-color', '#ff6b6b'),实现品牌色一键切换。
6.2 用<iframe>安全嵌入,规避 CSS 冲突
若要把问卷嵌入到已有网站(如公司内网),最安全的方式是 iframe:
<iframe src="survey.html" width="100%" height="600" frameborder="0" sandbox="allow-scripts allow-forms" ></iframe>sandbox属性至关重要:allow-scripts允许执行 JS,allow-forms允许提交,但默认禁止top-navigation、popups等危险行为,防止 XSS。问卷 HTML 文件内必须移除所有外部 CSS/JS 引用,所有样式内联到<style>,所有脚本内联到<script>,确保 iframe 内部完全自治。
6.3 用localStorage持久化用户填写进度
问卷较长时,用户刷新页面会丢失填写内容。用localStorage自动保存:
// 页面加载时恢复 document.querySelectorAll('input, textarea, select').forEach(el => { const key = `survey_${el.name || el.id}`; if (localStorage.getItem(key)) { el.value = localStorage.getItem(key); } }); // 输入时保存 ['input', 'textarea', 'select'].forEach(tag => { document.querySelectorAll(tag).forEach(el => { el.addEventListener('input', () => { const key = `survey_${el.name || el.id}`; localStorage.setItem(key, el.value); }); }); });注意:localStorage有 5MB 限制,且同域共享。若问卷含大量富文本,改用sessionStorage(关闭标签页即清除)更安全。
我习惯在每次写完一个表单区块后,立刻打开 Chrome 的 Lighthouse 面板跑一次「Accessibility」审计——它会直接标出label未关联、aria-invalid缺失、对比度不足等问题。这比靠人眼检查快十倍。这份「模仿问卷星个人版 HTML+CSS」不是练手玩具,而是你下次重构老系统表单时,能直接拆解复用的原子模块库。希望帮到你。
本文还有配套的精品资源,点击获取