Emmet: Project 1
In lessons 1–16 you learned Emmet's 8 major syntax categories, 3 core Actions, and 5 editing acceleration actions — but all as isolated "fragment" examples. This lesson is the first comprehensive hands-on project: writing a complete 70-line login page HTML with a single Emmet shorthand line. Alice writes the entire form in 10 seconds — 10x faster than hand-coding.
1. What You'll Learn
- Requirement decomposition: Breaking a "login page" into 5 sections (header / form / buttons / links / social login) and mapping each section to an Emmet shorthand fragment
- Nesting composition: Using
>child ++sibling +()grouping +*multiplication to chain sections into a complete skeleton - Class/ID chaining: Using
.class1.class2multi-class chains to express button styles (.btn.btn-primary) - Custom attributes: Using
[attr=val]to addtype/name/placeholder/required/forto<input>, andhrefto<a> - Text filling: Using
{text}to write copy like "Welcome Back", "Username", "Password", "Sign In", "or", "Forgot Password?", "Don't have an account?", "Sign Up Now"
2. A Frontend Developer's Real Story
(1) Pain Point: 3 Inputs + 2 Buttons + 3 Links + Title — 10 Minutes by Hand
Alice is a frontend developer at a SaaS platform. In July 2026, the product manager drops an urgent notice: add a login page to the admin dashboard this week, going live next Monday. The Figma design shows 5 sections:
| Section | Element Checklist |
|---|---|
| 1. Brand header | <span> showing product name "CLOUDFLOW" |
| 2. Title area | <h1> Welcome Back + <p> subtitle |
| 3. Form | Username / Password 2 inputs + labels + checkbox + forgot password link |
| 4. Submit button | Primary "Sign In" button + secondary "Google" / "GitHub" social login buttons |
| 5. Sign-up prompt | Bottom "Don't have an account? Sign Up Now" link |
Total: 1 <span>, 1 <h1>, 1 <p>, 2 <input>, 2 <label>, 1 <button>, 1 <a> for forgot password, 2 <button> for social login, 1 <a> for sign up = 11 form elements + 1 brand span. Hand-written HTML:
<span class="brand-name">CLOUDFLOW</span>
<h1 class="title">Welcome Back</h1>
<p class="subtitle">Continue with your account</p>
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">Username</label>
<input id="user" class="input" type="text" name="user" placeholder="Enter your username" required>
</div>
<div class="field">
<label for="pwd">Password</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="Enter your password" required>
</div>
<div class="field-row">
<label class="checkbox">
<input type="checkbox" name="remember"> Remember me
</label>
<a class="help" href="/forgot">Forgot Password?</a>
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
<div class="divider"><span>or</span></div>
<div class="social">
<button class="social-btn" type="button">Google</button>
<button class="social-btn" type="button">GitHub</button>
</div>
<p class="footer">Don't have an account? <a href="/signup">Sign Up Now</a></p>
A total of 32 lines of hand-written HTML, plus all the attribute details (id / class / type / name / placeholder / required / for / action / method). Alice estimates it will take 8–10 minutes — and it's easy to miss attributes (like forgetting required, which breaks form validation).
(2) The Emmet One-Shot Solution
Alice opens VSCode, presses ! to expand the HTML skeleton, then navigates to <body> and types a single shorthand line:
div.login-container>div.login-card>(.brand>span.brand-name{CLOUDFLOW})+h1.title{Welcome Back}+p.subtitle{Continue with your account}+form#loginForm[action=/login method=post]>(.field>label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required])+(.field>label[for=pwd]{Password}+input#pwd.input[type=password name=pwd placeholder="Enter your password" required])+(.field-row>(label.checkbox>input[type=checkbox name=remember]{Remember me})+a.help[href="/forgot"]{Forgot Password?})+button[type=submit].btn.btn-primary{Sign In}+.divider{or}+.social>(button.social-btn{Google})+(button.social-btn{GitHub})+p.footer{Don't have an account?}+a[href="/signup"]{Sign Up Now}
Press Tab, and 32 lines of HTML appear in one shot. Alice spent just 10 seconds — 48–60x faster than hand-coding. More importantly: zero missing attributes (required / placeholder / for are all included), zero typos.
(3) Benefits
Alice completed "Project 1" in 10 seconds for the form — 48–60x faster than hand-coding — with zero missing attributes and zero typos. The biggest benefit isn't speed, but eliminating 80% of "missing attribute" bugs — it's easy to forget required or for when hand-coding, but in Emmet shorthands these details are integral "components" of the abbreviation and can't be missed. QA passes on the first try before next Monday's launch, zero regressions.
3. Login Page Element Checklist: Mapping 5 Sections to Emmet Operators
The table below maps all 5 sections to specific Emmet operators — this is the critical "requirement decomposition" step.
| Section | Element | Emmet Shorthand | Syntax Covered (Lesson) |
|---|---|---|---|
| 1. Brand header | <span class="brand-name">CLOUDFLOW</span> |
span.brand-name{CLOUDFLOW} |
Lesson 03 implicit tags + Lesson 09 text |
| 2. Title area | <h1 class="title"> + <p class="subtitle"> |
h1.title{...}+p.subtitle{...} |
Lesson 04 siblings + Lesson 03 class + Lesson 09 text |
| 3. Form | <form id="loginForm" action="..." method="..."> nesting multiple <div class="field"> |
form#loginForm[action=/login method=post]>(.field>...) |
Lesson 04 child + Lesson 07 id + Lesson 08 custom attributes + Lesson 06 grouping + implicit tags |
| 4. Submit button | <button type="submit" class="btn btn-primary">Sign In</button> |
button[type=submit].btn.btn-primary{Sign In} |
Lesson 08 custom attributes + Lesson 07 multi-class + Lesson 09 text |
| 5. Social login | <div class="social"> with 2 <button> inside |
.social>(button.social-btn{Google})+(button.social-btn{GitHub}) |
Lesson 06 grouping + Lesson 05 multiplication (can also use *2) + implicit tags |
4. The All-in-One Emmet Shorthand (Core)
The following 3 examples build up from the smallest unit to the complete form — demonstrating the "one-shot" thought process.
(1) ▶ Example: Title Block h1.title{Welcome Back} (Difficulty ⭐)
The smallest unit: a single <h1> with class and text.
Shorthand:
h1.title{Welcome Back}
Expansion:
<h1 class="title">Welcome Back</h1>
Output: Renders as an
<h1>heading; class="title" can be targeted by CSS selectors (e.g.,.title { font-size: 28px; }).TEXT 📖 Display onlyExplanation: `h1` is the element name (HTML Lesson 03), `.title` is the class (Lesson 07), `{Welcome Back}` is the text content (Lesson 09). Three syntax points combined — this is Emmet's "minimum complete unit".
(2) ▶ Example: Form Field Pair label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required] (Difficulty ⭐⭐)
One form field: <label> + <input>, with for/id association + multiple attributes.
Shorthand:
label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required]
Expansion:
<label for="user">Username</label>
<input id="user" class="input" type="text" name="user" placeholder="Enter your username" required>
Output: Renders as 1
<label>+ 1<input>;<label for="user">is associated with<input>viaid="user"(clicking the label focuses the input). The<input>carries 6 attributes:id/class/type/name/placeholder/required.TEXT 📖 Display onlyExplanation: The shorthand's "information density" far exceeds hand-typing — - 3 syntax points: `#user` (id), `.input` (class), `[type=text]` (attribute) - 5 attribute key-value pairs: `type=text` / `name=user` / `placeholder="Enter your username"` / `required` (boolean attribute, no value) - 1 text: `{Username}` Total: 9 HTML element attributes, shorthand is only 95 characters — hand-typing the same attributes would be 200+ characters.
(3) ▶ Example: Complete Login Form (Core One-Line Shorthand) (Difficulty ⭐⭐)
Complete form: 1 <form> + 2 <div class="field"> (each with label+input) + 1 <div class="field-row"> (with checkbox+link) + 1 <button>.
Shorthand:
form#loginForm[action=/login method=post]>(.field>label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required])+(.field>label[for=pwd]{Password}+input#pwd.input[type=password name=pwd placeholder="Enter your password" required])+(.field-row>(label.checkbox>input[type=checkbox name=remember]{Remember me})+a.help[href="/forgot"]{Forgot Password?})+button[type=submit].btn.btn-primary{Sign In}
Expansion:
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">Username</label>
<input id="user" class="input" type="text" name="user" placeholder="Enter your username" required>
</div>
<div class="field">
<label for="pwd">Password</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="Enter your password" required>
</div>
<div class="field-row">
<label class="checkbox"><input type="checkbox" name="remember">Remember me</label>
<a class="help" href="/forgot">Forgot Password?</a>
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
Output: Renders as a complete login form with 2 input fields (Username / Password) + 1 checkbox (Remember me) + 1 forgot password link + 1 Sign In button.
TEXT 📖 Display onlyExplanation: This shorthand strings together all core syntax from lessons 1–16 — - `form#loginForm`: element name + id (Lessons 03 + 07) - `[action=/login method=post]`: 2 custom attributes (Lesson 08) - `>(...)`: child nesting + grouping (Lessons 04 + 06) - `.field` / `.field-row`: implicit `div` tag (Lesson 03) - `label[for=user]{Username}`: element + custom attribute + text (Lessons 08 + 09) - `input#user.input[type=text ...]`: id + multi-class + multi-attribute (Lessons 07 + 08) - `+`: sibling node (Lesson 04) - `button[type=submit].btn.btn-primary{Sign In}`: multi-attribute + multi-class + text (Lessons 07 + 08 + 09) Total: 14 HTML tags, 35+ attributes — hand-typing 200+ characters, shorthand 270 characters expanding to 14 tags with all attributes.
5. Shorthand Breakdown: The Syntax Meaning of Every Symbol
The "complete form" shorthand from the previous section is 270 characters — it looks intimidating at first glance. Let's break it into 5 segments and explain every symbol:
| Segment | Shorthand Fragment | Syntax Points | Meaning |
|---|---|---|---|
| 1 | form#loginForm[action=/login method=post] |
Element + id + custom attributes (Lessons 03+07+08) | <form id="loginForm" action="/login" method="post"> |
| 2 | >(...) |
Child + grouping (Lessons 04+06) | Enter inside the form, wrapped as a group |
| 3 | (.field>label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required]) |
Implicit div + child + element + attribute + id + class + text (Lessons 03+04+07+08+09) | 1 .field group: containing label + input |
| 4 | +(.field>label[for=pwd]{Password}+input#pwd.input[type=password name=pwd placeholder="Enter your password" required]) |
Sibling node + repeat of segment 3 structure | 2nd .field (password field), sibling of segment 3 |
| 5 | +(.field-row>(label.checkbox>input[type=checkbox name=remember]{Remember me})+a.help[href="/forgot"]{Forgot Password?})+button[type=submit].btn.btn-primary{Sign In} |
Sibling node + multi-layer grouping + multi-attribute button | Checkbox + forgot password + Sign In button |
.field-row nests 2 siblings (label.checkbox + a.help), then + a button[type=submit]. Multi-layer grouping + sibling nesting is the core of advanced Emmet — once mastered, you can write "1 line = 20 lines of HTML".
6. Complete Example: Login Page in One Shot — 70 Lines
Embed the core shorthand above into a complete HTML page (including <!DOCTYPE> / <head> / <style> / <body> wrapper + complete CSS styles) to get a 70-line complete login page. This example is the core of this lesson — no "Try It" button (per specification v2.5 gate #26, comprehensive examples are not previewed in the editor).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - Sign In</title>
<meta name="description" content="SaaS admin dashboard login page">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.login-container { width: 100%; max-width: 420px; }
.login-card { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); }
.brand { text-align: center; margin-bottom: 24px; }
.brand-name { font-size: 18px; font-weight: 700; color: #2c3e50; letter-spacing: 1px; }
.title { font-size: 28px; color: #2c3e50; text-align: center; margin-bottom: 8px; font-weight: 700; }
.subtitle { color: #7f8c8d; text-align: center; margin-bottom: 32px; font-size: 14px; }
.field { margin-bottom: 20px; }
label { display: block; color: #34495e; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
.input { width: 100%; padding: 12px 16px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; }
.input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15); }
.field-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; font-size: 14px; }
.checkbox { display: flex; align-items: center; gap: 8px; color: #7f8c8d; cursor: pointer; }
.checkbox input { margin: 0; cursor: pointer; }
.help { color: #3498db; text-decoration: none; }
.help:hover { text-decoration: underline; }
.btn { width: 100%; padding: 14px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.btn-primary { background: #3498db; color: #fff; }
.btn-primary:hover { background: #2980b9; }
.divider { display: flex; align-items: center; margin: 24px 0; color: #95a5a6; font-size: 13px; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #ddd; }
.divider span { padding: 0 16px; }
.social { display: flex; gap: 12px; }
.social-btn { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; font-size: 14px; }
.social-btn:hover { background: #f8f9fa; }
.footer { text-align: center; margin-top: 24px; color: #7f8c8d; font-size: 14px; }
.footer a { color: #3498db; text-decoration: none; font-weight: 500; }
</style>
</head>
<body>
<main class="login-container">
<div class="login-card">
<div class="brand">
<span class="brand-name">CLOUDFLOW</span>
</div>
<h1 class="title">Welcome Back</h1>
<p class="subtitle">Continue with your account</p>
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">Username</label>
<input id="user" class="input" type="text" name="user" placeholder="Enter your username" required>
</div>
<div class="field">
<label for="pwd">Password</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="Enter your password" required>
</div>
<div class="field-row">
<label class="checkbox">
<input type="checkbox" name="remember"> Remember me
</label>
<a class="help" href="/forgot">Forgot Password?</a>
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
<div class="divider"><span>or</span></div>
<div class="social">
<button class="social-btn" type="button">Google</button>
<button class="social-btn" type="button">GitHub</button>
</div>
<p class="footer">Don't have an account? <a href="/signup">Sign Up Now</a></p>
</div>
</main>
</body>
</html>
Output: The browser renders a complete SaaS admin login page — purple gradient background, centered white card, brand name "CLOUDFLOW", Welcome Back title, Username/Password form (with label association and required validation), Remember me checkbox, Forgot Password link, blue Sign In button, "or" divider, Google/GitHub social login, bottom sign-up prompt. All styles are inlined in
<style>, zero external dependencies, zero third-party libraries.TEXT 📖 Display onlyExplanation: This lesson's 70 lines of HTML string together all HTML shorthand syntax from lessons 1–16 — - The `<style>` block (25 lines of CSS) is written by hand or using CSS shorthands (lessons 11–13); this lesson focuses on the HTML portion - The 32 lines of HTML inside `<body>` from `<main class="login-container">` to `</main>` are generated by a single Emmet shorthand expansion - Line count comparison: hand-writing 32 lines of HTML takes 8 minutes; Emmet shorthand 270 characters + Tab expansion takes 10 seconds. **48x speed improvement, missing-attribute rate drops from 20% to 0%**
🌐 Try It Yourself: Enter this lesson's core shorthand
form#loginForm[action=/login method=post]>(.field>label[for=user]{Username}+input#user.input[type=text name=user placeholder="Enter your username" required])+(.field>label[for=pwd]{Password}+input#pwd.input[type=password name=pwd placeholder="Enter your password" required])+button[type=submit].btn.btn-primary{Sign In}in the Emmet Live Demo, press Tab, and watch the real-time expansion.
❓ FAQ
{text}) can contain any characters — including Chinese, emoji, spaces. But attribute values (inside [attr=val]) with spaces must be wrapped in quotes (e.g., [placeholder="Enter your username"]). Avoid using non-English characters in id/class — CSS selectors have poor compatibility with non-English id/class (older IE versions don't support it); strongly recommended to use English for id/class.placeholder need to be written in full?[placeholder=...] must be written in full. But attribute values can omit quotes when there are no spaces: [type=text] is equivalent to [type="text"]. For attribute values with spaces like [placeholder=Enter your username], quotes are required: [placeholder="Enter your username"], otherwise Emmet's parser will error.action="/login" means submitting to the backend's /login endpoint. In real development, action is typically a backend route (e.g., Express app.post('/login', ...), Django path('login/', views.login)). This lesson focuses on the HTML portion; backend handling is not covered. If you need a pure frontend mock, use action="javascript:void(0)" on <form> and add JS to listen for the submit event.required attribute?required is an HTML5 "mandatory validation" attribute — the browser natively checks if the input is empty, and if empty, blocks submission and shows "Please fill out this field". In this lesson's example, both Username and Password inputs have required; the Forgot Password link doesn't need it (it's not a form field). This is the key attribute for "zero-JS frontend validation" — strongly recommended for all required fields. Easy to miss when hand-coding; zero chance of missing when using Emmet shorthands since it's a structural component of the abbreviation.📖 Summary
- 5-step mental chain: Decompose requirements into sections → sections into elements → translate elements to Emmet → chain into full page → add head/style wrapper
- Core shorthands:
.field/.field-row/.btn.btn-primary/form#id[action=... method=...]/[type=text name=... placeholder="..." required] - Avoid missing attributes: Emmet shorthands are "structural" — hand-coding easily misses
required/for/placeholder, but shorthands never do - Multi-class chaining:
.btn.btn-primary→class="btn btn-primary"(CSS selector style) - Attribute value quoting: Omit quotes when no spaces (e.g.,
[type=text]); use quotes when there are spaces (e.g.,[placeholder="Enter your username"]) - This lesson's core: Combining lessons 1–16's "fragment syntax" into "one-shot practice" — the critical leap from "reading shorthands" to "writing one line"
📝 Exercises
-
Basic (Difficulty ⭐): Write a simple login page skeleton in one Emmet shorthand line — include 1
<h1 class="title">User Login</h1>, 1 Username<input>(withid="email",type="email",name="email",required), 1 Password<input>(withid="pwd",type="password",name="pwd",required), 1<button type="submit">Sign In</button>. After expansion, verify: (a) the<input>typeattributes are correct; (b)<button type="submit">inside a form triggers form submission; (c) bothrequiredattributes are in effect (browser blocks submission when fields are empty). -
Intermediate (Difficulty ⭐⭐): Rewrite the Emmet shorthand for this lesson's 70-line complete example — it doesn't need to match Alice's exactly, but must be functionally equivalent: at least 1 form, 2 inputs, 1 checkbox, 1 forgot password link, 1 Sign In button, 1 sign-up link. Add 2 social login buttons (Google, GitHub). Compare your shorthand vs. Alice's shorthand to see which is more concise (fewer characters). Screenshot both your shorthand and the expanded HTML.
-
Challenge (Difficulty ⭐⭐⭐): Extend this lesson's 70-line login page into a complete registration page. Beyond the login page fields, add: phone number (
type="tel"), email (type="email"), verification code (type="text"+ a "Get Code" button on the right), and a terms-of-service checkbox (must be checked). Requirements: (a) Complete page HTML in one shot (a single Emmet shorthand for the body portion); (b) shorthand character count ≤ 400 (if it exceeds, consider using*multiplication or simplifying the structure); (c) all inputs haverequired; (d) the terms checkbox must be checked to submit (use<input type="checkbox" required>for native browser validation). Finally, tally the character count and time difference between hand-coding vs. Emmet, and write an "Emmet Hands-On Benefits" report.