`.
+8. **Putting a long paragraph inside a label.** Move extra help to `aria-describedby` text.
+9. **Using `` or `` in place of ``.** It looks the same, but it is not connected.
+10. **Wrong order of text and fields** that makes the label ambiguous (for example, a label between two fields).
+
+## Practice Time
+
+Try these in your editor:
+
+1. Build a form with name, email, and message fields, each with an explicit label using `for` and `id`.
+2. Click each label, and confirm the matching field gets focus.
+3. Rewrite one field with a wrapping label instead.
+4. Add a checkbox, and make its label clickable.
+5. Replace a placeholder-only field with a label plus a placeholder example.
+6. Create a search field with a visually hidden label.
+7. Add a hint under a password field, and connect it with `aria-describedby`.
+8. Mark one field as required with `required` and the text "(required)" in the label.
+9. Break a `for` on purpose, then notice the click behavior stops. Fix it.
+10. Bonus: open your browser's Accessibility panel, and check the "name" shown for each field.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What does the `` element do?
+
+It gives a form control its visible and accessible name. Clicking the label activates the control, and screen readers announce the label text for it.
+
+
+
+
+2. How do you connect a label to a field?
+
+Either with `for` on the label matching the `id` on the field, or by wrapping the field inside the ``.
+
+
+
+
+3. What is the difference between `id` and `name` on a field?
+
+`id` identifies the field on the page, for labels, CSS, and scripts. `name` is the key under which the field's value is sent to the server.
+
+
+
+
+4. Why is a placeholder not a replacement for a label?
+
+It disappears when typing, has low contrast, may not be announced as the name, and cannot be clicked to focus the field. Use it only for a short format example.
+
+
+
+
+5. How can you hide a label visually but keep it accessible?
+
+Use a visually hidden CSS class, so the label stays in the page for assistive technology. Do not use `display: none`. `aria-label` is a simpler alternative with some trade-offs.
+
+
+
+
+6. How do you connect a hint to a field?
+
+Give the hint an `id`, and add `aria-describedby="that-id"` to the field. Screen readers then read the hint after the label.
+
+
+
+## Key Takeaways
+
+- Every form control needs a label. A missing one is the most common form accessibility failure.
+- Connect a label with `for` and a matching `id`, or by wrapping the control. Never use `name` in `for`.
+- Labels enlarge the click and tap target, give fields an accessible name, and support voice control.
+- A placeholder is not a label. Use it only for a format example.
+- If a design hides a label, hide it visually with CSS, never with `display: none`.
+- Put hints and errors in separate text linked with `aria-describedby`, and mark required fields in words, not by color alone.
+- Write short, plain labels that translate well.
+
+Now your fields have names. Next, learn the element that does the real work: the field itself, in [Input Element](./input-element), or return to the [Forms overview](/tutorials/html/forms).
\ No newline at end of file
diff --git a/tutorials/html/08-forms/min-and-max.mdx b/tutorials/html/08-forms/min-and-max.mdx
index 90789543c..ffb362a39 100644
--- a/tutorials/html/08-forms/min-and-max.mdx
+++ b/tutorials/html/08-forms/min-and-max.mdx
@@ -1,16 +1,535 @@
---
-id: min-and-max
-title: "Numerical and Date Range Bounds: min and max Attributes"
-sidebar_label: "Min & Max Attributes"
-sidebar_position: 15
+id: html-min-and-max
+title: "HTML Min, Max, and Step Attributes"
+sidebar_label: "Min and Max"
+sidebar_position: 16
slug: /html/forms/min-and-max
-description: "Establish lower and upper bound thresholds for numeric, range, and date/time inputs using min and max attributes."
+description: "Learn the HTML min, max, and step attributes: limiting numbers, ranges, dates, times, months, and weeks, how the step base works, the rangeUnderflow, rangeOverflow, and stepMismatch validity flags, setting today as a minimum date, linking two date fields, money and decimal pitfalls, and why the server must check ranges again."
keywords:
- - HTML min attribute
- - HTML max attribute
- - number bounds validation
- - date range limit HTML
+ - HTML min max attribute
+ - HTML step attribute
+ - input type number min max
+ - input type date min max
+ - rangeUnderflow rangeOverflow
+ - stepMismatch
+ - step any
+ - restrict date range input
+ - HTML form validation range
+ - learn HTML forms
- CodeHarborHub HTML
-authors: [ajay-dhangar]
-tags: [html, forms, validation, attributes]
----
\ No newline at end of file
+tags: [html, web-development, beginner, forms, validation, min, max, step, frontend]
+---
+
+import {useState, useEffect} from 'react';
+
+export function FlagList({validity}) {
+ const names = ["valueMissing", "badInput", "rangeUnderflow", "rangeOverflow", "stepMismatch", "valid"];
+ if (!validity) return Type a value to see the flags.
;
+ return (
+
+ {names.map((name) => (
+
+ {name}: {String(validity[name])}
+
+ ))}
+
+ );
+}
+
+export function NumberDemo() {
+ const [qty, setQty] = useState(null);
+ const [pct, setPct] = useState(null);
+ const copy = (v) => {
+ const out = {};
+ ["valueMissing", "badInput", "rangeUnderflow", "rangeOverflow", "stepMismatch", "valid"].forEach((k) => (out[k] = v[k]));
+ return out;
+ };
+ return (
+
+ );
+}
+
+export function pad(n) {
+ return String(n).padStart(2, "0");
+}
+
+export function toInputDate(date) {
+ return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
+}
+
+export function StayDatesDemo() {
+ const [bounds, setBounds] = useState({today: "", last: ""});
+ const [arrive, setArrive] = useState("");
+ const [leave, setLeave] = useState("");
+ const [message, setMessage] = useState("");
+
+ useEffect(() => {
+ const now = new Date();
+ const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 60);
+ setBounds({today: toInputDate(now), last: toInputDate(end)});
+ }, []);
+
+ const leaveMin = arrive || bounds.today;
+
+ return (
+
+ );
+}
+
+Plenty of fields have a **sensible range**. A quantity must be at least 1. A rating goes from 1 to 5. A booking cannot be in the past. A volume slider runs from 0 to 100. HTML gives you three attributes for this: **`min`** and **`max`** set the smallest and largest allowed value, and **`step`** controls which values in between are allowed.
+
+They work on numbers, dates, times, months, weeks, and sliders. This lesson shows how each behaves, how the `step` rule surprises people, and how to read the validity flags when something is out of range.
+
+
+
+
+## What You Will Learn
+
+- What `min`, `max`, and `step` do, and which input types support them
+- How the browser reacts to a value that is too small, too large, or off-step
+- The `step` base rule, and `step="any"`
+- Limits for dates and times, including "today" and linked date fields
+- The `rangeUnderflow`, `rangeOverflow`, and `stepMismatch` flags
+- Pitfalls with decimals and money
+- Why the server must check ranges again
+
+## The Basics
+
+
+
+
+```html title="index.html"
+
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+The up and down arrows stay between 1 and 10. Type `0` or `11` and press **Add to cart**: the browser blocks the form with a message such as "Value must be greater than or equal to 1."
+
+| Attribute | Meaning |
+| :-- | :-- |
+| `min` | The **smallest** allowed value |
+| `max` | The **largest** allowed value |
+| `step` | The **spacing** between allowed values. Valid values are `min` plus a whole number of steps |
+
+## Where They Work
+
+| Input type | Value format for `min` and `max` | Default `step` |
+| :-- | :-- | :-- |
+| `number` | A number, like `1` or `0.5` | `1` |
+| `range` | A number (defaults: min `0`, max `100`) | `1` |
+| `date` | `yyyy-mm-dd`, like `2026-12-25` | 1 day |
+| `month` | `yyyy-mm`, like `2026-12` | 1 month |
+| `week` | `yyyy-Www`, like `2026-W52` | 1 week |
+| `time` | `HH:mm`, like `09:30` | 60 seconds |
+| `datetime-local` | `yyyy-mm-ddTHH:mm` | 60 seconds |
+| `text`, `email`, `url`, `tel`, `password`, `search` | **Not supported.** Use `minlength` and `maxlength` | |
+| `checkbox`, `radio`, `file`, `select`, `textarea` | **Not supported** | |
+
+Formats are always the **machine** formats shown above, no matter how the visitor's browser displays them. See the lessons on [Date](./input-types/date), [Time](./input-types/time), [Month](./input-types/month), [Week](./input-types/week), and [Number](./input-types/number).
+
+## What the Browser Does
+
+- **Spinner arrows and pickers** stay within the range. A date picker greys out days outside `min` and `max`.
+- **Typing is not blocked.** A visitor can still type `99` into a field with `max="10"`. The check happens **when the form is submitted**, with a message.
+- **An empty field passes.** `min` and `max` only check values that exist. Add `required` if a value is mandatory.
+- **The validity flags** tell your script what went wrong (below).
+
+## Try It: Watch the Flags
+
+Type values into both fields (try `0`, `5`, `11`, `2.5`, `12`, `abc`), and watch which flags switch on.
+
+
+
+
+```html title="index.html"
+Quantity (1 to 10, whole numbers)
+
+
+Discount (0 to 50, in steps of 5)
+
+
+
+```
+
+```js title="script.js"
+const fields = [document.getElementById("qty"), document.getElementById("pct")];
+const out = document.getElementById("out");
+
+function show() {
+ out.textContent = fields
+ .map((field) => {
+ const v = field.validity;
+ return field.name + ": underflow=" + v.rangeUnderflow +
+ ", overflow=" + v.rangeOverflow + ", stepMismatch=" + v.stepMismatch;
+ })
+ .join("\n");
+}
+
+fields.forEach((field) => field.addEventListener("input", show));
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+| Flag | `true` when | Example |
+| :-- | :-- | :-- |
+| `rangeUnderflow` | The value is **below** `min` | `0` with `min="1"` |
+| `rangeOverflow` | The value is **above** `max` | `11` with `max="10"` |
+| `stepMismatch` | The value is **not on a valid step** | `12` with `step="5"` |
+| `badInput` | The browser cannot read what was typed | Letters in a number field |
+| `valueMissing` | Empty and `required` | No value |
+
+## Understanding `step`
+
+`step` is the part that surprises people. Valid values are the **step base** plus a whole number of steps. The step base is:
+
+1. the `min` value, if there is one,
+2. otherwise the `value` attribute, if there is one,
+3. otherwise `0` (or the type's usual starting point).
+
+| Markup | Valid values |
+| :-- | :-- |
+| `step="5"` (no min) | `0`, `5`, `10`, `15`... and `-5`, `-10`... |
+| `min="1" step="5"` | `1`, `6`, `11`, `16`... (**not** 5 or 10) |
+| `min="0" max="1" step="0.25"` | `0`, `0.25`, `0.5`, `0.75`, `1` |
+| `step="any"` | **Any** number, with no step check at all |
+
+So `min="1" step="5"` surprises people: the step counts **from `min`**, not from zero.
+
+### `step="any"`
+
+By default, `type="number"` accepts **whole numbers only** (step 1). A decimal like `2.5` gives a `stepMismatch` error. Two ways to allow decimals:
+
+```html
+
+
+```
+
+### Steps for dates and times
+
+| Type | Unit of `step` | Example |
+| :-- | :-- | :-- |
+| `date` | **Days** | `step="7"` allows every 7th day from the step base |
+| `month` | **Months** | `step="3"` for quarters |
+| `week` | **Weeks** | `step="2"` |
+| `time` | **Seconds** | `step="900"` allows every 15 minutes |
+| `datetime-local` | **Seconds** | `step="1800"` for half-hour slots |
+
+For a time field, setting `step` to less than 60 (such as `step="1"`) also shows the **seconds** box.
+
+## Dates: "From Today" and Linked Fields
+
+A very common need is "no dates in the past," and "the end must not be before the start." The page has to calculate the date, because HTML has no keyword for "today."
+
+Try it. The arrival date is limited to the next 60 days, and the departure picker moves its minimum to match the arrival.
+
+
+
+
+```html title="index.html"
+
+```
+
+```js title="script.js"
+const arrive = document.getElementById("arrive");
+const leave = document.getElementById("leave");
+
+// Format a Date as yyyy-mm-dd in the visitor's local time
+function toInputDate(date) {
+ const pad = (n) => String(n).padStart(2, "0");
+ return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
+}
+
+const now = new Date();
+const last = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 60);
+
+arrive.min = toInputDate(now);
+arrive.max = toInputDate(last);
+leave.min = toInputDate(now);
+leave.max = toInputDate(last);
+
+// The departure can never be before the arrival
+arrive.addEventListener("change", () => {
+ if (arrive.value) leave.min = arrive.value;
+});
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+:::warning Do not use `toISOString()` for "today"
+`new Date().toISOString().slice(0, 10)` gives today's date in **UTC**, which can be yesterday or tomorrow for the visitor. Build the string from the **local** year, month, and day, as in the example above.
+:::
+
+### Times that cross midnight
+
+For a `time` field, a `min` that is **later** than `max` is allowed and means an overnight range. For example, `min="22:00" max="06:00"` accepts late evening and early morning. For every other type, a `min` larger than `max` makes the field impossible to fill in correctly, so avoid it. (See [Time](./input-types/time).)
+
+## Decimals and Money
+
+Computers store decimals in binary, so `0.1 + 0.2` is `0.30000000000000004`. With `step`, browsers allow for this, but your own code may not.
+
+- **Money:** `type="number" step="0.01"` works for typing. But in your code and on the server, store money as **whole cents** (or the smallest unit), not as floating-point numbers.
+- **Do not compare decimals with `===`** in JavaScript. Round first, or work in whole units.
+- **Currencies differ.** Some have no minor unit (the Japanese yen), and some have three decimal places (the Kuwaiti dinar). Do not hard-code two decimals if you serve several currencies.
+
+## Slider Ranges
+
+`type="range"` always has a value, so `min`, `max`, and `step` define the whole slider. The defaults are `0`, `100`, and `1`.
+
+```html
+Volume
+
+```
+
+A slider cannot be left empty, so it never gives `valueMissing`. (See [Range](./input-types/range).)
+
+## Styling
+
+```css title="style.css"
+input:out-of-range {
+ border-color: #b91c1c;
+}
+
+input:in-range {
+ border-color: #15803d;
+}
+```
+
+`:in-range` and `:out-of-range` match fields that have `min` or `max` and a value inside or outside it. As always, add a **text message** too. Colour alone is not enough. For fields that should not look red before the visitor has typed, `:user-invalid` is gentler (see [Form Validation](./form-validation)).
+
+## Accessibility at a Glance
+
+- **Say the range in the label or a hint:** "Quantity (1 to 10)". Screen readers also announce `min` and `max` for numbers and sliders.
+- **Write helpful messages:** "Choose a date from today to 60 days ahead," not "Out of range."
+- **Use the right input type.** A date picker is more accessible than three number fields, and gives keyboard users arrows and shortcuts.
+- **Keep the picker's range and your message in sync** when `min` or `max` change by script.
+- **Do not make a range impossible** (for example, a departure `min` that is after its `max`).
+- **Tell visitors why a date is unavailable** ("Fully booked"), if you block dates in other ways.
+- **Offer a text alternative** for dates and times if the browser's picker is hard to use for your audience.
+
+## Security Notes
+
+- **`min`, `max`, and `step` are not security.** A visitor can edit them in the developer tools, or send a request with any value by hand.
+- **Check every range on the server,** including prices, quantities, discounts, and dates. Never trust a number the browser sends: a `quantity` of `-5` or a `discount` of `100` can cause real harm.
+- **Do not take the price from the form.** Look up the price on the server by product ID.
+- **Check business limits the browser cannot know,** such as stock left, booking availability, or the maximum order per customer.
+- **Watch for huge and special numbers:** very large values, `NaN`, and scientific notation (`1e9`) can cause trouble in code. Convert and range-check them.
+
+See [Secure Form Design](./secure-form-design).
+
+## Global Audience Notes
+
+- **Displays differ, values do not.** A visitor in one country sees `25/12/2026`, another sees `12/25/2026`, and both are sent as `2026-12-25`. Set `min` and `max` in the machine format.
+- **Number formats differ.** Some visitors type `1,5` for one and a half, and the browser handles it when the input type is `number`. Read the value on the server as a plain number, not as text.
+- **Weeks start on different days,** and the `week` type uses ISO weeks. Explain this if it matters.
+- **Time zones:** "today" depends on where the visitor is. Check on the server with a clear time zone rule, and store dates in UTC or with their zone.
+- **Calendars:** some visitors use calendars other than the Gregorian one. The value is always Gregorian, even when the picker displays another calendar.
+- **Browser support** for `month` and `week` is incomplete, so plan a fallback.
+
+## Common Mistakes to Avoid
+
+1. **Trusting `min` and `max` as security.** The server must check ranges.
+2. **Using them on text inputs,** where they do nothing. Use `minlength` and `maxlength`.
+3. **Forgetting that `step` counts from `min`.** With `min="1" step="5"`, `5` is not valid.
+4. **Getting `stepMismatch` on decimals** because `step` is `1`. Use `step="0.01"` or `"any"`.
+5. **Writing dates in the wrong format,** instead of `yyyy-mm-dd`.
+6. **Using `toISOString()`** for "today," which uses UTC.
+7. **Setting `min` larger than `max`,** so no value is valid (except for time).
+8. **Expecting typing to be blocked.** The check happens on submit.
+9. **Expecting an empty field to fail.** Add `required`.
+10. **Storing money as floating-point numbers.**
+11. **Taking prices or totals from the form.**
+12. **Not telling visitors the range,** so errors are a surprise.
+
+## Practice Time
+
+Try these in your editor:
+
+1. Make a number field with `min="1"`, `max="10"`, and test both ends.
+2. Type a decimal into it and see `stepMismatch`. Then add `step="0.5"`.
+3. Try `min="1" step="5"` and find which values are valid.
+4. Switch to `step="any"` and compare.
+5. Build a `date` field that cannot be before today, set by script.
+6. Add a second date that cannot be before the first.
+7. Make a `time` field that only allows every 15 minutes.
+8. Make a slider from 0 to 10 and show its value with ``.
+9. Read `validity.rangeUnderflow` and `rangeOverflow` in the console, and write your own messages.
+10. Bonus: write the server-side check for a quantity from 1 to 10 and a booking date within 60 days.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What do `min`, `max`, and `step` do?
+
+`min` and `max` set the smallest and largest allowed value, and `step` sets the spacing between valid values. They work on number, range, date, month, week, time, and datetime-local inputs.
+
+
+
+
+2. Do they work on text inputs?
+
+No. For text, use `minlength` and `maxlength`, which limit the number of characters.
+
+
+
+
+3. Where does `step` count from?
+
+From the step base: the `min` value if there is one, otherwise the `value` attribute, otherwise zero. So `min="1" step="5"` allows 1, 6, 11, and so on.
+
+
+
+
+4. How do you allow any decimal in a number field?
+
+Use `step="any"`, or a specific step such as `step="0.01"`. The default step of 1 allows whole numbers only.
+
+
+
+
+5. How do you stop visitors choosing a past date?
+
+Set `min` to today's date, in `yyyy-mm-dd` format, using the visitor's **local** date (not `toISOString()`). HTML has no built-in "today" keyword, so a little script is needed.
+
+
+
+
+6. Are `min` and `max` enough to protect a price or quantity?
+
+No. Anyone can bypass them. The server must check every number and date, and should not accept prices from the form at all.
+
+
+
+## Key Takeaways
+
+- `min`, `max`, and `step` limit the **value** of number, range, date, month, week, time, and datetime-local inputs.
+- Out-of-range values give `rangeUnderflow` or `rangeOverflow`, and off-step values give `stepMismatch`.
+- The `step` base is `min`, so `min="1" step="5"` allows 1, 6, 11. Use `step="any"` or a decimal step for decimals.
+- The check runs on submit, not while typing, and empty fields pass unless `required`.
+- Set date limits by script in `yyyy-mm-dd` format using local time, and link start and end fields.
+- Store money as whole cents, and never take prices from the form.
+- These attributes are a convenience. **The server must check every range again.**
+
+Next, learn how browsers fill in forms for visitors, in [Autocomplete](./autocomplete), or return to the [Forms overview](/tutorials/html/forms).
\ No newline at end of file
diff --git a/tutorials/html/08-forms/minlength-and-maxlength.mdx b/tutorials/html/08-forms/minlength-and-maxlength.mdx
index 1f5f6fede..203f68ccf 100644
--- a/tutorials/html/08-forms/minlength-and-maxlength.mdx
+++ b/tutorials/html/08-forms/minlength-and-maxlength.mdx
@@ -1,16 +1,488 @@
---
-id: minlength-and-maxlength
-title: "String Length Limits: minlength and maxlength Attributes"
-sidebar_label: "Minlength & Maxlength"
-sidebar_position: 14
+id: html-minlength-and-maxlength
+title: "HTML Minlength and Maxlength Attributes"
+sidebar_label: "Minlength and Maxlength"
+sidebar_position: 15
slug: /html/forms/minlength-and-maxlength
-description: "Restrict minimum and maximum character counts in text fields and textareas using minlength and maxlength attributes."
+description: "Learn the HTML minlength and maxlength attributes: setting minimum and maximum text lengths on inputs and textareas, how browsers count characters (UTF-16 code units, emoji, and line breaks), the tooShort and tooLong validity flags, accessible character counters, sensible limits for passwords and names, and why the server must enforce limits too."
keywords:
- - HTML minlength attribute
- - HTML maxlength attribute
- - text input character limit
- - enforce string bounds HTML
+ - HTML minlength maxlength
+ - maxlength attribute
+ - minlength attribute
+ - character counter textarea
+ - tooShort tooLong validity
+ - input length limit
+ - HTML form validation length
+ - maxlength emoji
+ - learn HTML forms
- CodeHarborHub HTML
-authors: [ajay-dhangar]
-tags: [html, forms, validation, attributes]
----
\ No newline at end of file
+tags: [html, web-development, beginner, forms, validation, minlength, maxlength, frontend]
+---
+
+import {useState} from 'react';
+
+export function CounterDemo() {
+ const min = 20;
+ const max = 120;
+ const [text, setText] = useState("");
+ const [touched, setTouched] = useState(false);
+ const length = text.length;
+ const left = max - length;
+ let hint = "Write at least " + min + " characters.";
+ let color = "#64748b";
+ if (length >= min) {
+ hint = "Long enough.";
+ color = "#15803d";
+ } else if (touched && length > 0) {
+ hint = (min - length) + " more characters needed.";
+ color = "#b91c1c";
+ }
+ return (
+
+ );
+}
+
+export function CountingDemo() {
+ const [text, setText] = useState("Hi 👋 café");
+ const codeUnits = text.length;
+ const codePoints = Array.from(text).length;
+ let graphemes = codePoints;
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
+ graphemes = Array.from(new Intl.Segmenter(undefined, {granularity: "grapheme"}).segment(text)).length;
+ }
+ const bytes = new TextEncoder().encode(text).length;
+ const cell = {padding: "0.2rem 0.75rem 0.2rem 0", borderBottom: "1px solid #e2e8f0"};
+ return (
+ event.preventDefault()}>
+ Type anything (try emoji, accents, or 李雷)
+ setText(event.target.value)}
+ style={{width: "100%", maxWidth: "28rem"}}
+ />
+
+
+ JavaScript .length (what maxlength counts) {codeUnits}
+ Unicode code points {codePoints}
+ Visible characters (graphemes) {graphemes}
+ Bytes in UTF-8 (what a database may count) {bytes}
+
+
+
+ );
+}
+
+Almost every text field needs a sensible size. A username that is one letter long is useless. A "bio" that is a million characters long can break your page or your database. HTML gives you two attributes for this: **`minlength`** sets the shortest allowed text, and **`maxlength`** sets the longest.
+
+They look simple, but they have a few surprises: what exactly is counted, when each one is checked, and why a hard limit can frustrate visitors. This lesson explains all of it.
+
+
+
+
+## What You Will Learn
+
+- What `minlength` and `maxlength` do, and where they work
+- How the browser enforces each one
+- How characters are counted (and why emoji can count double)
+- The `tooShort` and `tooLong` validity flags
+- How to build an accessible character counter
+- Choosing sensible limits
+- Why the server must enforce limits too
+
+## The Basics
+
+
+
+
+```html title="index.html"
+
+ Username (3 to 12 characters)
+
+
+ Join
+
+```
+
+
+
+
+
+<>
+ event.preventDefault()}>
+ Username (3 to 12 characters)
+ {" "}
+ Join
+
+>
+
+
+
+
+
+Try it: type `ab` and press **Join**, and the browser says the text is too short. Then type a long word, and notice you **cannot type past 12 characters**.
+
+| Attribute | Meaning | What the browser does |
+| :-- | :-- | :-- |
+| `minlength="3"` | At least 3 characters | Blocks the submit and shows a message when the text is shorter |
+| `maxlength="12"` | At most 12 characters | **Stops the visitor from typing** more than 12 |
+
+## Where They Work
+
+| Control | `minlength` / `maxlength` |
+| :-- | :-- |
+| `text`, `search`, `tel`, `url`, `email`, `password` | **Yes** |
+| `` | **Yes** |
+| `type="number"`, `range`, `date`, `time` | **No.** Use `min` and `max` for **values** (see [Min and Max](./min-and-max)) |
+| ``, checkbox, radio, file | No |
+
+:::note Length is not the same as value
+`maxlength="3"` on a text field limits **how many characters** can be typed. `max="999"` on a number field limits **how big the number** can be. They are different tools.
+:::
+
+## How Each One Behaves
+
+### `maxlength`: a hard stop while typing
+
+The browser simply refuses extra characters, so a visitor can never submit text that is too long **by typing**. Two details to know:
+
+- **Pasting:** if someone pastes text that is too long, browsers usually **cut it off silently** at the limit. The visitor may not notice that the end of their text is gone.
+- **Script-set values:** if your JavaScript sets `input.value` to a longer text, the browser allows it, but the field becomes invalid (`tooLong`) when validated.
+
+### `minlength`: checked when submitting
+
+The browser checks `minlength` only on submit (or when your script asks). Two details:
+
+- **An empty field passes.** Add `required` if the field must have a value. (`minlength="3"` alone does **not** make the field required.)
+- **Only after the visitor edits it.** A **pre-filled** value that is too short is not flagged until the visitor changes it, so do not rely on `minlength` to catch bad stored data.
+
+### Together
+
+You can combine them with other rules. The field must pass **all** of them:
+
+```html
+
+```
+
+Make sure `minlength` is **not larger** than `maxlength`, or the field can never be valid.
+
+## Try It: A Character Counter
+
+This textarea needs 20 to 120 characters. Watch the counter, the hint, and what happens when you type or paste past the limit.
+
+
+
+
+```html title="index.html"
+Short bio (20 to 120 characters)
+
+
+
+
+```
+
+```js title="script.js"
+const bio = document.getElementById("bio");
+const count = document.getElementById("bio-count");
+const hint = document.getElementById("bio-hint");
+const min = Number(bio.minLength);
+const max = Number(bio.maxLength);
+
+function update() {
+ const length = bio.value.length;
+ count.textContent = length + " / " + max + " characters (" + (max - length) + " left)";
+ hint.textContent = length >= min ? "Long enough." : "Write at least " + min + " characters.";
+}
+
+bio.addEventListener("input", update);
+update();
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+### Making the Counter Accessible
+
+- **Link it to the field** with `aria-describedby`, so screen readers read the limit with the field.
+- **Do not announce every key press.** Putting `aria-live="polite"` on a counter that changes with every character makes a screen reader chatter constantly. Instead, announce only at key moments: when the visitor is close to the limit (say, 20 left), and when they reach it.
+- **Do not rely on colour alone.** Say "12 characters left" in words.
+- **Show the limit up front** ("120 characters maximum"), not only after the visitor reaches it.
+
+## How Characters Are Counted
+
+This is where beginners get surprised. Browsers measure `maxlength` in **UTF-16 code units**, the same units as JavaScript's `.length`. Most letters count as 1, but many emoji and rare characters count as **2**.
+
+Type different characters below, and compare the counts:
+
+
+
+
+```html title="index.html"
+Type anything
+
+
+```
+
+```js title="script.js"
+const input = document.getElementById("sample");
+const out = document.getElementById("out");
+
+function show() {
+ const text = input.value;
+ const units = text.length; // what maxlength counts
+ const points = Array.from(text).length; // Unicode code points
+ const graphemes = Array.from(
+ new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)
+ ).length; // what people see
+ const bytes = new TextEncoder().encode(text).length; // UTF-8 bytes
+
+ out.textContent =
+ "length: " + units + "\ncode points: " + points +
+ "\nvisible characters: " + graphemes + "\nUTF-8 bytes: " + bytes;
+}
+
+input.addEventListener("input", show);
+show();
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+What this means for you:
+
+| Unit | Example | Why it matters |
+| :-- | :-- | :-- |
+| UTF-16 code units | `👋` is **2** | What `maxlength` and `.length` count |
+| Code points | `👋` is 1 | What many languages (Python, for example) count |
+| Visible characters | `👨👩👧` is 1 (but 8 code units) | What the visitor thinks of as "one character" |
+| UTF-8 bytes | `é` is 2, `李` is 3, `👋` is 4 | What many databases limit |
+
+So a visitor who types four emoji into a field with `maxlength="8"` has used up all 8. Keep your limits **generous**, so these differences do not matter, and **count the same way on the server** (or in bytes, if your database limit is in bytes).
+
+### Line Breaks in a Textarea
+
+In a `textarea`, the browser counts a line break as **one** character for `maxlength`. But when the form is **submitted**, line breaks are sent as two characters (carriage return and line feed). So the server may see a text a little longer than the visitor typed, one extra character per line. Allow some room on the server, or normalise line breaks before checking.
+
+## The `tooShort` and `tooLong` Flags
+
+| Flag | `true` when | Notes |
+| :-- | :-- | :-- |
+| `validity.tooShort` | Shorter than `minlength` | Only after the visitor has edited the field, and only when not empty |
+| `validity.tooLong` | Longer than `maxlength` | Rare, because typing is blocked. Happens with values set by a script |
+| `validity.valueMissing` | Empty and `required` | Use `required` for this |
+
+You can use them to write a clear message:
+
+```js
+const input = document.getElementById("username");
+
+input.addEventListener("input", () => {
+ if (input.validity.tooShort) {
+ input.setCustomValidity("Use at least " + input.minLength + " characters. You have used " + input.value.length + ".");
+ } else {
+ input.setCustomValidity("");
+ }
+});
+```
+
+Always clear the message with an empty string. (See [Form Validation](./form-validation).)
+
+## Choosing Good Limits
+
+| Field | Suggested limits | Why |
+| :-- | :-- | :-- |
+| Username | 3 to 30 | Short enough to display, long enough to be unique |
+| Full name | **No short minimum**, maximum 100 or more | Some real names are one or two characters, and some are very long |
+| Email address | Maximum 254 | The practical limit for an address |
+| Password | **Minimum 8** (or more), maximum **at least 64**, or none | Length is the best defence. Never set a low maximum |
+| Phone number | Maximum about 20 | Allows `+`, spaces, and extensions |
+| Postal code | Maximum about 12 | Varies widely by country |
+| Tweet-style message | A real limit, shown clearly | The limit is part of the design |
+| Comment or bio | Maximum 500 to 5000 | Stops abuse, but still lets people write |
+| Search | Maximum about 200 | Prevents huge queries |
+
+Tips:
+
+- **Do not set a minimum for names.** A one-letter name, or a name in a script where one character is a whole word, is perfectly valid.
+- **Do not set a low maximum on passwords.** A visitor who uses a password manager may have a very long, strong password.
+- **Tell visitors the limit.** A hidden limit that silently cuts their text is frustrating.
+- **Avoid cutting off text silently.** If paste is likely, consider showing a counter and letting the visitor go over, with an error message, instead of using `maxlength`. That way they can edit rather than lose text.
+
+:::tip Hard limit or soft limit?
+`maxlength` is a **hard** limit: typing stops. For long, thoughtful text, a **soft** limit is kinder: leave out `maxlength`, show the counter turning red when over, and block only on submit. The visitor can then paste a long text and trim it themselves.
+:::
+
+## Accessibility at a Glance
+
+- **State the limits in text** before the visitor types ("3 to 12 characters").
+- **Link counters and hints** with `aria-describedby`.
+- **Avoid chatty live regions.** Announce only near the limit.
+- **Do not cut text silently.** If pasted text is shortened, say so.
+- **Give clear errors:** "Use at least 8 characters. You have used 5."
+- **Do not use `maxlength` as the only way** to show a limit. Some assistive technology does not announce it.
+- **Respect autofill and password managers.** Do not set limits that break them.
+
+## Security Notes
+
+- **These attributes are not security.** A visitor can remove them in the developer tools or send a request by hand. The **server must enforce every limit**.
+- **A maximum protects your server.** Without a server-side limit, someone can send megabytes of text in one field, filling your database or slowing your app.
+- **Limit the whole request,** too, not only each field (your web server can set a maximum body size).
+- **Check the unit your database uses.** A column limited to 50 **bytes** can hold far fewer than 50 characters of non-Latin text.
+- **Do not cut strings blindly** on the server in the middle of a multi-part character, which creates broken text. Cut by code point or visible character.
+- **A limit is not a defence against injection.** Escape output and use parameterised queries.
+
+See [Secure Form Design](./secure-form-design).
+
+## Global Audience Notes
+
+- **Count fairly.** A name or message in Chinese, Japanese, or Korean carries much more meaning per character than one in English, so a "100 character" limit means very different amounts of text. Think about whether your limit makes sense for every language.
+- **Emoji and combined characters** use several units. Keep limits generous.
+- **Do not set a minimum** for names, where a single character can be valid.
+- **Translate your counter text** ("120 characters left") and use the visitor's number formatting if you show large numbers.
+- **Right-to-left text** works the same way for counting, but check that your counter's layout looks right when mirrored.
+
+## Common Mistakes to Avoid
+
+1. **Relying on `maxlength` alone** for safety. The server must enforce limits.
+2. **Expecting `minlength` to make a field required.** Empty fields pass. Add `required`.
+3. **Setting `minlength` larger than `maxlength`,** so the field is never valid.
+4. **Using `maxlength` on `type="number"`,** where it does nothing. Use `max`.
+5. **Silently cutting pasted text.**
+6. **A very low maximum on passwords.**
+7. **A minimum on names.**
+8. **Counting emoji as one** and then being surprised that the limit is reached early.
+9. **A counter that announces every key press** to screen readers.
+10. **Forgetting that databases count bytes,** not characters.
+11. **Not telling visitors the limit** until they hit it.
+12. **Trusting that a pre-filled short value** will be flagged by `minlength`.
+
+## Practice Time
+
+Try these in your editor:
+
+1. Make a username field with `minlength="3"` and `maxlength="12"`, and test it.
+2. Remove `required` and see that an empty field passes `minlength`.
+3. Try to type, then paste, a text longer than `maxlength`. What happens?
+4. Add a live character counter to a textarea, linked with `aria-describedby`.
+5. Make the counter announce only when 20 characters are left, using a separate `aria-live` message.
+6. Type an emoji into a field with `maxlength="2"`, and count how many fit.
+7. Show `validity.tooShort` in the console as you type.
+8. Replace the browser message with your own, and clear it correctly.
+9. Build a "soft limit" textarea: no `maxlength`, a counter that turns red when over, and a block only on submit.
+10. Bonus: write the server-side check for your username rule, counting in the same way as the browser.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What do `minlength` and `maxlength` do?
+
+`minlength` sets the shortest allowed text, and `maxlength` sets the longest. `maxlength` stops the visitor typing past the limit, and `minlength` is checked when the form is submitted.
+
+
+
+
+2. Does `minlength` make a field required?
+
+No. An empty field passes `minlength`. Add `required` if a value is mandatory.
+
+
+
+
+3. Which controls support them?
+
+Text-like inputs (`text`, `search`, `tel`, `url`, `email`, `password`) and `textarea`. For numbers, use `min` and `max` instead.
+
+
+
+
+4. How does the browser count characters?
+
+In UTF-16 code units, like JavaScript's `.length`. Most characters count as 1, but many emoji count as 2. Databases may count UTF-8 bytes, which is different again.
+
+
+
+
+5. What is a good way to handle long text such as a bio?
+
+Show a counter and consider a soft limit: do not set `maxlength`, let the counter warn when the text is over, and block on submit. That way pasted text is not silently cut off.
+
+
+
+
+6. Is `maxlength` enough to protect the server?
+
+No. Anyone can bypass it. The server must enforce every limit, including a limit on the size of the whole request.
+
+
+
+## Key Takeaways
+
+- `minlength` and `maxlength` limit the **number of characters** in text-like inputs and textareas.
+- `maxlength` blocks typing past the limit. `minlength` is checked on submit, ignores empty fields, and is only flagged after the visitor edits the value.
+- Characters are counted in UTF-16 code units, so emoji can count as 2, and databases may count bytes.
+- Use `validity.tooShort` and `validity.tooLong` to write clear, specific messages.
+- Show the limits in words, and keep counters quiet for screen readers.
+- Choose generous limits: no minimum for names, and no low maximum for passwords.
+- These attributes are a convenience. **The server must enforce every limit.**
+
+Next, learn how to limit numbers and dates, in [Min and Max](./min-and-max), or return to the [Forms overview](/tutorials/html/forms).
\ No newline at end of file
diff --git a/tutorials/html/08-forms/multipart-form-data.mdx b/tutorials/html/08-forms/multipart-form-data.mdx
index c905b1140..0a799102e 100644
--- a/tutorials/html/08-forms/multipart-form-data.mdx
+++ b/tutorials/html/08-forms/multipart-form-data.mdx
@@ -1,16 +1,523 @@
---
-id: multipart-form-data
-title: "Handling File Submissions: enctype='multipart/form-data'"
-sidebar_label: "Form Encoding (enctype)"
-sidebar_position: 19
+id: html-multipart-form-data
+title: "HTML Multipart Form Data (enctype)"
+sidebar_label: "Multipart Form Data"
+sidebar_position: 20
slug: /html/forms/multipart-form-data
-description: "Master form encoding types (enctype) including application/x-www-form-urlencoded and multipart/form-data for binary file uploads."
+description: "Learn the HTML form enctype attribute: application/x-www-form-urlencoded, multipart/form-data, and text/plain, why file uploads need multipart, what a multipart request body looks like, using FormData with fetch without setting the Content-Type, formenctype on buttons, and safe server-side handling of uploaded files, size limits, and filenames."
keywords:
- - HTML enctype attribute
- - multipart form data
- - file upload form encoding
- - form binary data submission
+ - HTML enctype
+ - multipart/form-data
+ - form enctype attribute
+ - file upload HTML form
+ - FormData fetch upload
+ - application/x-www-form-urlencoded
+ - formenctype
+ - multipart boundary
+ - upload file security
+ - learn HTML forms
- CodeHarborHub HTML
-authors: [ajay-dhangar]
-tags: [html, forms, enctype, file-upload]
----
\ No newline at end of file
+tags: [html, web-development, beginner, forms, enctype, file-upload, multipart, security, frontend]
+---
+
+import {useState} from 'react';
+
+export function EnctypeDemo() {
+ const [enctype, setEnctype] = useState("multipart/form-data");
+ const [out, setOut] = useState(null);
+ return (
+ {
+ event.preventDefault();
+ const form = event.currentTarget;
+ const fd = new FormData();
+ fd.append("title", form.elements.title.value);
+ fd.append("note", form.elements.note.value);
+ const file = form.elements.photo.files[0];
+ if (file) fd.append("photo", file);
+ else fd.append("photo", new File([], ""));
+ let header = "";
+ let body = "";
+ try {
+ if (enctype === "multipart/form-data") {
+ const request = new Request("https://example.com/upload", {method: "POST", body: fd});
+ header = request.headers.get("content-type") || "multipart/form-data";
+ body = await request.text();
+ } else if (enctype === "application/x-www-form-urlencoded") {
+ const params = new URLSearchParams();
+ for (const [key, value] of fd.entries()) {
+ params.append(key, value instanceof File ? value.name : value);
+ }
+ header = "application/x-www-form-urlencoded";
+ body = params.toString();
+ } else {
+ const lines = [];
+ for (const [key, value] of fd.entries()) {
+ lines.push(key + "=" + (value instanceof File ? value.name : value));
+ }
+ header = "text/plain";
+ body = lines.join("\r\n");
+ }
+ } catch (error) {
+ header = "(could not build a preview in this browser)";
+ }
+ if (body.length > 1200) body = body.slice(0, 1200) + "\n… (cut short for display)";
+ setOut({header, body});
+ }}
+ >
+
+ enctype
+ setEnctype(event.target.value)}>
+ application/x-www-form-urlencoded (default)
+ multipart/form-data
+ text/plain
+
+
+
+ Title
+
+
+
+ Note
+
+
+
+ Pick a small text file (optional)
+
+
+ Show the request body
+ {out ? (
+
+
Content-Type: {out.header}
+
{out.body}
+
+ ) : null}
+
+ );
+}
+
+export function FileCheckDemo() {
+ const maxBytes = 2 * 1024 * 1024;
+ const allowed = ["image/png", "image/jpeg", "application/pdf"];
+ const [rows, setRows] = useState([]);
+ const size = (bytes) => (bytes < 1024 ? bytes + " B" : bytes < 1048576 ? (bytes / 1024).toFixed(1) + " KB" : (bytes / 1048576).toFixed(1) + " MB");
+ return (
+ event.preventDefault()}>
+ Choose files (PNG, JPEG, or PDF, up to 2 MB each)
+ {
+ const list = Array.from(event.target.files).map((file) => {
+ let problem = "";
+ if (file.size > maxBytes) problem = "Too big";
+ else if (!allowed.includes(file.type)) problem = "Type not allowed (" + (file.type || "unknown") + ")";
+ return {name: file.name, size: size(file.size), problem};
+ });
+ setRows(list);
+ }}
+ />
+
+ {rows.length === 0 ? No files chosen yet. : null}
+ {rows.map((row, i) => (
+
+ {row.name} ({row.size}): {row.problem || "looks fine in the browser"}
+
+ ))}
+
+
+ );
+}
+
+Most forms send short pieces of text. But what if a visitor wants to **upload a photo**, a PDF, or a video? Plain text encoding cannot carry a file, so HTML gives you a way to change how the data is **packaged**. That setting is the **`enctype`** attribute (short for *encoding type*), and its most important value is **`multipart/form-data`**.
+
+Forgetting this one attribute is the number one reason file uploads "do not work" for beginners: the form submits, and the server receives only the file's **name**, with no file at all. After this lesson, you will never make that mistake.
+
+
+
+
+## What You Will Learn
+
+- What `enctype` is and the three values it can have
+- Why file uploads need `multipart/form-data`
+- What a multipart request actually looks like
+- How `FormData` and `fetch` handle it, and the one thing you must not do
+- Overriding it with `formenctype`
+- How to check files in the browser, and why the server must check again
+- Safe handling of uploaded files
+
+## The Basics
+
+
+
+
+```html title="index.html"
+
+ Title
+
+
+ Photo
+
+
+ Upload
+
+```
+
+
+
+
+
+<>
+ event.preventDefault()}>
+ Title
+
+ Photo
+
+ Upload
+
+>
+
+
+
+
+
+The two requirements for a file upload are:
+
+1. **`method="post"`**. Files cannot travel in a URL, so GET cannot upload files.
+2. **`enctype="multipart/form-data"`**. This packages each field, including the file's bytes, as a separate **part**.
+
+## The Three Values
+
+| `enctype` | What it does | Use it for |
+| :-- | :-- | :-- |
+| `application/x-www-form-urlencoded` | **The default.** Builds one string, `name=value&name2=value2`, with special characters encoded (`%C3%A9`) | Ordinary forms with only text |
+| `multipart/form-data` | Sends each field as its own part, with room for raw file bytes | **Any form with a file input** |
+| `text/plain` | Sends `name=value` lines, with no encoding | Almost nothing. It is hard for a server to read reliably |
+
+`enctype` only matters for **POST** forms. With `method="get"`, the data always goes into the URL, and `enctype` is ignored.
+
+:::warning The classic mistake
+Without `enctype="multipart/form-data"`, a form with ` ` still submits, but the server receives **only the file's name** as plain text, never the file. There is no error message. If your uploads are mysteriously empty, check this first.
+:::
+
+## Try It: See What Is Sent
+
+Switch between the three `enctype` values and press the button. The demo builds the real request body the browser would produce, without sending it. Pick a small text file, and notice what happens to it in each mode.
+
+
+
+
+```html title="index.html"
+
+
+
+
+ Send
+
+```
+
+```js title="script.js"
+const form = document.getElementById("f");
+
+form.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ // Build the same request the browser would send, without sending it
+ const request = new Request("https://example.com/upload", {
+ method: "POST",
+ body: new FormData(form),
+ });
+
+ console.log(request.headers.get("content-type")); // includes the boundary
+ console.log(await request.text()); // the raw body
+});
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+## What a Multipart Body Looks Like
+
+A multipart body is a series of **parts** separated by a long random **boundary** string that the browser invents so it never clashes with your data:
+
+```text
+Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAbC123
+
+------WebKitFormBoundaryAbC123
+Content-Disposition: form-data; name="title"
+
+Café menu
+------WebKitFormBoundaryAbC123
+Content-Disposition: form-data; name="photo"; filename="menu.png"
+Content-Type: image/png
+
+(the raw bytes of the file go here)
+------WebKitFormBoundaryAbC123--
+```
+
+Each part has its own small header:
+
+| Part header | What it tells the server |
+| :-- | :-- |
+| `name="photo"` | Which form field this part came from (the input's `name`) |
+| `filename="menu.png"` | The file's original name, **as the visitor's computer calls it** (do not trust it) |
+| `Content-Type: image/png` | The type the **browser guessed**, usually from the extension (do not trust it either) |
+
+You never write this yourself. The browser builds it, and your server framework reads it and gives you the fields and files separately. The boundary appears in the `Content-Type` header so the server knows where each part ends.
+
+### Why Not Use the Default?
+
+The default encoding turns **every** character into `%XX` codes. A 5 MB photo would become over 15 MB of mostly `%` signs, and binary bytes could not be represented safely. Multipart sends the file's bytes **as they are**, which is smaller and exact.
+
+For plain text forms, multipart works, too, but is slightly larger. So use it **only when you need a file**.
+
+## Using `fetch` and `FormData`
+
+When you upload with JavaScript, `FormData` builds the multipart body for you:
+
+```js title="script.js"
+const form = document.getElementById("upload");
+
+form.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ const response = await fetch("/upload", {
+ method: "POST",
+ body: new FormData(form), // multipart, with boundary, built for you
+ });
+
+ console.log(response.ok ? "Uploaded" : "Failed");
+});
+```
+
+:::danger Do not set the Content-Type header yourself
+When the body is a `FormData`, **leave the `Content-Type` header out**. The browser adds `multipart/form-data` **with the correct boundary**. If you write `headers: { "Content-Type": "multipart/form-data" }` by hand, the boundary is missing, and the server cannot read the body. This is the most common `fetch` upload bug.
+:::
+
+You can add extra values to a `FormData` before sending:
+
+```js
+const data = new FormData(form);
+data.append("source", "profile-page");
+data.append("photo", someBlob, "avatar.png"); // a file you made in code
+```
+
+### Showing Progress
+
+`fetch` does not report upload progress. For a progress bar, use `XMLHttpRequest` and its `upload.onprogress` event, or a library. For very large files, split them into chunks, or use direct-to-storage uploads, as below.
+
+## One Form, Different Encodings: `formenctype`
+
+A submit button can override the form's encoding for itself:
+
+```html
+
+
+
+
+ Save with photo
+ Save text only
+
+```
+
+See [Submit and Reset Buttons](./input-types/submit-reset-button).
+
+## Checking Files in the Browser
+
+The browser can show visitors a problem **before** a slow upload starts. Pick some files below, and see the checks:
+
+
+
+
+```html title="index.html"
+Choose files (PNG, JPEG, or PDF, up to 2 MB each)
+
+
+```
+
+```js title="script.js"
+const input = document.getElementById("files");
+const list = document.getElementById("list");
+const maxBytes = 2 * 1024 * 1024;
+const allowed = ["image/png", "image/jpeg", "application/pdf"];
+
+input.addEventListener("change", () => {
+ list.innerHTML = "";
+
+ for (const file of input.files) {
+ const item = document.createElement("li");
+ let problem = "";
+
+ if (file.size > maxBytes) problem = "Too big";
+ else if (!allowed.includes(file.type)) problem = "Type not allowed";
+
+ item.textContent = file.name + " (" + file.size + " bytes): " + (problem || "looks fine");
+ list.append(item);
+ }
+});
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+These checks are **kindness, not security**. `file.type` comes from the file's extension, and anyone can rename `virus.exe` to `photo.png`. Details are in the [File input lesson](./input-types/file).
+
+## Handling Uploads Safely on the Server
+
+An upload is the most dangerous thing a form can do, because you are letting a stranger put a file on your computer. The server must treat every uploaded file as hostile.
+
+| Risk | What to do |
+| :-- | :-- |
+| **Huge files** that fill the disk or memory | Set a **maximum request size** on the web server, and a limit in your application. Stop reading when it is exceeded |
+| **Fake types** (a script named `photo.png`) | Check the real type from the file's **first bytes** ("magic numbers"), not the name or the browser's `Content-Type`. Allow only types you need |
+| **Dangerous filenames** (`../../etc/passwd`, `shell.php`, names with null bytes) | **Never use the visitor's filename** to save the file. Generate your own name (a random ID) and keep the original only as text, escaped, for display |
+| **Running uploaded code** | Store files **outside** the web root, or on separate storage that **cannot execute** scripts. Never serve user uploads from the same place as your code |
+| **Malware** | Scan with antivirus, especially if other people will download the files |
+| **Image tricks** (huge dimensions, hidden data, scripts in SVG) | Re-process images with a trusted library (resize and re-encode), and treat SVG as code, not as a picture |
+| **Serving files back unsafely** | Send a correct `Content-Type`, add `X-Content-Type-Options: nosniff`, and use `Content-Disposition: attachment` for downloads |
+| **Privacy** | Remove metadata (location in photos). Control who may see each file |
+| **Abuse** | Require sign-in, rate-limit uploads, and set a quota per user |
+| **Slow uploads** (many open connections) | Set timeouts, and use a reverse proxy |
+
+For large files, a common modern design is to have the server give the browser a **short-lived signed URL** so the file goes **straight to cloud storage**, never touching your application server. The server then checks and records the result.
+
+See [Secure Form Design](./secure-form-design).
+
+## Accessibility at a Glance
+
+- **Label the file input** clearly, and say what is allowed: "Photo (PNG or JPEG, up to 2 MB)."
+- **Show what was chosen:** the file name and size, in text, so screen reader users know the selection worked.
+- **Give feedback during and after upload** in a `role="status"` area ("Uploading… Done"), not only a visual bar.
+- **Do not rely on drag and drop alone.** Keep the normal file button available for keyboard and screen reader users.
+- **Explain errors** in words: "That file is 5 MB. The limit is 2 MB."
+- **Let visitors remove or replace a file** before sending.
+- **Keep the rest of the form's data** if the upload fails.
+
+## Security Notes at a Glance
+
+- **`enctype` is not security.** It only controls packaging.
+- **Everything in a multipart request can be forged:** the filename, the part's `Content-Type`, and the file's contents.
+- **The server must enforce** size, type, count, and permission checks.
+- **Protect against CSRF** on upload endpoints. A forged upload can fill your storage.
+- **Never `eval`, `include`, or execute** anything uploaded.
+- **Serve uploads from a different domain** (a cookie-free one) where possible, so a stored script cannot read your visitors' sessions.
+
+## Global Audience Notes
+
+- **Filenames contain any script:** accents, Chinese, Arabic, emoji. Browsers send them as UTF-8 in multipart, so your server must read them as UTF-8 and store them safely (or replace them with your own).
+- **File sizes on mobile networks:** photos from modern phones are several megabytes. Consider resizing in the browser before upload, and warn about data costs.
+- **Slow and unreliable connections** break big uploads. Offer retry, or resumable uploads.
+- **File type names** (such as "JPEG") may mean nothing to some visitors. Say "photo" and show examples.
+- **Legal rules** about stored images and documents differ by country (privacy, retention, and children's data). Tell visitors what you keep and for how long.
+
+## Common Mistakes to Avoid
+
+1. **Forgetting `enctype="multipart/form-data"`,** so only the filename arrives.
+2. **Using `method="get"`** with a file input.
+3. **Setting `Content-Type: multipart/form-data` by hand** in `fetch`, which loses the boundary.
+4. **Trusting `file.type`, the extension, or the filename** for security.
+5. **Saving files under the visitor's filename.**
+6. **Storing uploads inside the web root** where they can run.
+7. **No size limit** on the server.
+8. **Believing `accept` blocks other types.** It only guides the picker.
+9. **Using multipart for ordinary text forms** with no need.
+10. **Using `text/plain`** and expecting the server to parse it.
+11. **No upload CSRF protection or rate limit.**
+12. **Not telling the visitor** what is allowed, or what went wrong.
+
+## Practice Time
+
+Try these in your editor:
+
+1. Build a form with a file input and `method="post"`, but no `enctype`. Look at what the server (or the demo above) receives.
+2. Add `enctype="multipart/form-data"`, and compare.
+3. Use `new Request(url, { method: "POST", body: new FormData(form) })` and print its `content-type` and body.
+4. Upload a file with `fetch` and `FormData`, without setting any headers.
+5. Add a size and type check that shows a message before sending.
+6. Add a second submit button with `formenctype`.
+7. Let a visitor choose several files with `multiple`, and list their names and sizes.
+8. Write a message for a file that is too big, in plain words.
+9. Write down the safe-handling steps your server needs, in order, for an avatar upload.
+10. Bonus: look at an upload request in your browser's Network tab, and find the boundary and the parts.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What does `enctype` do?
+
+It decides how a POST form's data is packaged in the request body. The default is `application/x-www-form-urlencoded`. Use `multipart/form-data` for file uploads.
+
+
+
+
+2. What two things does a file upload form need?
+
+`method="post"` and `enctype="multipart/form-data"`. Without the second, only the file's name is sent, not the file.
+
+
+
+
+3. What is the boundary in a multipart request?
+
+A random string the browser creates to separate the parts. It is written into the `Content-Type` header so the server knows where each part starts and ends.
+
+
+
+
+4. What must you never do when using `fetch` with `FormData`?
+
+Never set the `Content-Type` header yourself. The browser adds it with the correct boundary. Setting it by hand removes the boundary, and the server cannot read the body.
+
+
+
+
+5. Why can't you trust the filename or the type of an uploaded file?
+
+Both come from the visitor's side, and can be forged or renamed. The server should generate its own filename, check the real type from the file's first bytes, and store the file where it cannot run.
+
+
+
+
+6. Does `enctype` make an upload safe?
+
+No. It only controls packaging. The server must enforce size, type, count, and permission limits, and protect against CSRF and abuse.
+
+
+
+## Key Takeaways
+
+- `enctype` sets how a POST body is encoded: `application/x-www-form-urlencoded` (default), `multipart/form-data`, or `text/plain`.
+- **File uploads need `method="post"` and `enctype="multipart/form-data"`.** Otherwise only the file name arrives.
+- A multipart body is a set of parts split by a boundary, each with a `name`, and files with a `filename` and a type.
+- With `fetch`, pass a `FormData` as the body and **do not set `Content-Type`**.
+- `formenctype` on a button overrides the encoding for that button.
+- Check files in the browser for kindness, and again on the server for safety: size, real type, safe names, safe storage.
+- Treat every upload as hostile, and tell visitors clearly what is allowed.
+
+Next, learn how to make every form usable by everyone, in [Accessible Forms](./accessible-forms), or return to the [Forms overview](/tutorials/html/forms).
\ No newline at end of file
diff --git a/tutorials/html/08-forms/option-and-optgroup.mdx b/tutorials/html/08-forms/option-and-optgroup.mdx
index df6981dec..1de900f91 100644
--- a/tutorials/html/08-forms/option-and-optgroup.mdx
+++ b/tutorials/html/08-forms/option-and-optgroup.mdx
@@ -1,16 +1,552 @@
---
-id: option-and-optgroup
-title: "Options and Grouping: option and optgroup Tags"
-sidebar_label: "Option & Optgroup"
-sidebar_position: 7
+id: html-option-and-optgroup
+title: "HTML Option and Optgroup Elements"
+sidebar_label: "Option and Optgroup"
+sidebar_position: 8
slug: /html/forms/option-and-optgroup
-description: "Define selectable list choices using option tags and organize large menu options logically using optgroup categories."
+description: "Learn the HTML option and optgroup elements: the value, selected, disabled, and label attributes, how text and value differ, grouping long lists with optgroup, disabled options and groups, how the default choice is picked, adding and removing options with JavaScript, accessibility, and common mistakes on CodeHarborHub."
keywords:
- - HTML option tag
- - HTML optgroup tag
- - select menu categories
- - grouping drop down options
+ - HTML option element
+ - HTML optgroup element
+ - option value selected disabled
+ - option label attribute
+ - optgroup label disabled
+ - group options in select
+ - select default option rules
+ - new Option JavaScript
+ - option text vs value
+ - disabled option placeholder
+ - learn HTML forms
- CodeHarborHub HTML
-authors: [ajay-dhangar]
-tags: [html, forms, options, select]
----
\ No newline at end of file
+tags: [html, web-development, beginner, forms, select, option, optgroup, accessibility, frontend]
+---
+
+import {useState} from 'react';
+
+export function CourseDemo() {
+ const [sent, setSent] = useState("(press Enrol)");
+ return (
+ {
+ event.preventDefault();
+ const text = new URLSearchParams(new FormData(event.currentTarget)).toString();
+ setSent(text || "(nothing is sent)");
+ }}
+ >
+ Choose a course
+
+ Select a course
+
+ HTML
+ CSS
+ JavaScript
+
+
+ SQL
+ Python for data
+
+
+ Android
+ iOS
+
+ Other (not available)
+ {" "}
+ Enrol
+ Sent: {sent}
+
+ );
+}
+
+export function InspectDemo() {
+ const [info, setInfo] = useState(null);
+ return (
+ event.preventDefault()}>
+ T-shirt size
+ {
+ const option = event.target.selectedOptions[0];
+ setInfo({
+ value: option.value,
+ text: option.text,
+ label: option.label,
+ index: option.index,
+ });
+ }}
+ >
+ Choose a size
+ Small (chest 86 to 91 cm)
+ Medium (chest 96 to 101 cm)
+ Large (chest 106 to 111 cm)
+
+
+ {info ? (
+ <>
+ option.value: {info.value || "(empty)"}
+ option.text: {info.text}
+ option.label: {info.label}
+ option.index: {info.index}
+ >
+ ) : "Choose an option to inspect it."}
+
+
+ );
+}
+
+A dropdown is only as good as the choices inside it. The `` you met in [Select](./select) is the container, and these two elements fill it. **``** is a single choice. **` `** gathers related options under a heading, so a long list is easy to scan.
+
+They are small elements with a handful of attributes, and a few rules that surprise people: why the text and the value are different things, how the browser picks the starting choice, and what happens to a disabled option. In this lesson, you will learn them all, and how to add and remove options with JavaScript.
+
+
+
+
+## What You Will Learn
+
+- The `` element, and its `value`, `selected`, `disabled`, and `label` attributes
+- The difference between an option's **text** and its **value**
+- How the browser decides which option starts selected
+- Grouping with ` `, including disabled groups
+- Adding, removing, and reading options with JavaScript
+- Accessibility, styling limits, and common mistakes
+
+## The Basics
+
+
+
+
+```html title="index.html"
+T-shirt size
+
+ Small
+ Medium
+ Large
+
+```
+
+
+
+
+
+<>
+ event.preventDefault()}>
+ T-shirt size
+
+ Small
+ Medium
+ Large
+
+
+>
+
+
+
+
+
+Each `` has two jobs:
+
+1. **The text** between its tags is what the visitor **reads**.
+2. **The `value`** is what your server **receives**.
+
+Options can only live inside a `` (or an ``, or a ``). The text must be **plain text**. You cannot put bold text, images, or links inside an option, because browsers draw the list themselves.
+
+:::note The end tag
+HTML allows you to leave out ` ` in some cases. Do not. Always write the end tag, so your code is clear and every tool reads it the same way.
+:::
+
+## The Option Attributes
+
+| Attribute | What it does | Example |
+| :-- | :-- | :-- |
+| `value` | What is sent when this option is chosen | `value="m"` |
+| `selected` | This option starts chosen | `selected` |
+| `disabled` | Cannot be chosen, and is never sent | `disabled` |
+| `label` | Text shown **instead of** the content, in the closed list and the list | `label="Medium"` |
+
+### `value`: What Is Sent
+
+```html
+Nigeria
+Nigeria
+```
+
+If you leave out `value`, the **text** is used as the value. That is risky when the text is translated or reworded, because your server would then receive different words for the same choice. Always give options a **stable `value`**, such as a code or an id, and let the text change freely.
+
+An **empty value** (`value=""`) is special. It is how you build the "placeholder" option, and it makes `required` work (see [Select](./select)).
+
+### Text, Value, and Label
+
+Each option actually has three pieces of information. Pick an option below, and see all of them.
+
+
+
+
+```html title="index.html"
+T-shirt size
+
+ Choose a size
+ Small (chest 86 to 91 cm)
+ Medium (chest 96 to 101 cm)
+ Large (chest 106 to 111 cm)
+
+
+```
+
+```js title="script.js"
+const select = document.getElementById("size");
+const info = document.getElementById("info");
+
+select.addEventListener("change", () => {
+ const option = select.selectedOptions[0];
+ info.textContent =
+ "value: " + (option.value || "(empty)") + " | " +
+ "text: " + option.text + " | " +
+ "label: " + option.label + " | " +
+ "index: " + option.index;
+});
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+| Property | Comes from | Used for |
+| :-- | :-- | :-- |
+| `option.value` | The `value` attribute (or the text, if missing) | What is **sent** |
+| `option.text` | The text between the tags | What the visitor reads in the list |
+| `option.label` | The `label` attribute (or the text, if missing) | A shorter name, shown in the closed box in some browsers |
+| `option.index` | The position in the list, counting from 0 | Finding an option in code |
+
+The `label` attribute is rarely needed. It lets you show a **short** name for the chosen option, while the opened list shows longer text. Browser support is uneven (some use it in the list too), so do not depend on it. If the long text is needed to choose, the visitor must be able to see it.
+
+### Whitespace
+
+The browser **trims** spaces at the start and end of option text, and collapses several spaces or line breaks into one. So indentation in your code does not change what people see. To show an indented option (as in a tree), use CSS padding on the option, or `optgroup`, and not spaces.
+
+## How the Starting Choice Is Picked
+
+This matters, and it has a few rules. For a normal, single-choice dropdown:
+
+1. If **one** option has `selected`, that one starts chosen.
+2. If **several** have `selected`, the **last** one wins.
+3. If **none** has `selected`, the browser picks the **first option that is not disabled**.
+
+Rule 3 has a consequence that catches many beginners. Suppose your placeholder is disabled:
+
+```html
+
+ Choose a country
+ India
+ Nigeria
+
+```
+
+Because the first option is disabled, the browser skips it, and **starts with India chosen**. The placeholder is never shown. To fix it, add `selected` to the disabled placeholder:
+
+```html
+Choose a country
+```
+
+For a ``, any number of options can start with `selected`, and none is chosen if you give none.
+
+## A Disabled Option Is Never Sent
+
+A disabled option is **greyed out** and cannot be clicked or chosen from the keyboard. It is useful for choices that exist but are not available now ("Sold out," "Coming soon").
+
+```html
+
+ Seat A1
+ Seat A2 (taken)
+ Seat A3
+
+```
+
+- A disabled option **cannot be submitted** by a normal visitor. But anyone can send `seat=a2` by hand, so **check on the server** that the choice is allowed.
+- Say **why** an option is disabled, in its text: "(taken)," "(sold out)." A grey option with no reason is frustrating.
+- If the **chosen** option is disabled (using `selected disabled`), the select sends **nothing** for that field.
+
+## Grouping with ``
+
+A long list is easier to scan when it has **headings**. `` wraps a set of options, and its `label` attribute is the heading.
+
+
+
+
+```html title="index.html"
+Choose a course
+
+ Select a course
+
+
+ HTML
+ CSS
+ JavaScript
+
+
+
+ SQL
+ Python for data
+
+
+
+ Android
+ iOS
+
+
+ Other (not available)
+
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+Open the list. You will see bold headings ("Web development," "Data") with the options indented below them. The "Mobile" group and the last option are greyed out. Choose "Python for data," and press Enrol: only `course=py` is sent, and the group name is **never** sent.
+
+### The Rules of ``
+
+| Rule | Detail |
+| :-- | :-- |
+| `label` is **required** | It is the heading. Without it, the group is useless |
+| The heading is **not selectable** | It is a label, not a choice. Screen readers announce it as a group |
+| `disabled` on the group | Disables **every** option inside it at once |
+| **No nesting** | An `` cannot contain another ``. HTML allows one level of grouping only |
+| Options can sit **outside** groups | You may mix ungrouped options (such as the placeholder) with groups |
+| Works with `multiple` | Groups work the same in a list box |
+| Only the **option's value** is sent | The group's label never reaches the server |
+
+If the server needs to know which group the choice came from, **look it up from the option's value**, or encode it in the value (such as `web-html`).
+
+### When to Use Groups
+
+Use `` when:
+
+- The list is **long** (roughly 10 or more options), and the options fall naturally into categories.
+- The visitor may not know the category names but recognises the options (for example, cities by country).
+
+Do not use it when:
+
+- There are only a few options. Headings just add clutter.
+- The groups would hold one option each.
+- You need **two levels** of grouping. Use dependent dropdowns (see [Select](./select)), or a different control.
+
+## Adding and Removing Options with JavaScript
+
+You can change a list while the page is open. The `Option` constructor makes a new option:
+
+```js
+// new Option(text, value, defaultSelected, selected)
+const option = new Option("Medium", "m");
+
+select.add(option); // add to the end
+select.add(option, 0); // add at the start
+select.options[2].remove(); // remove the third option
+select.remove(1); // remove the option at index 1
+select.length = 0; // remove all of them
+```
+
+A complete example, which fills a dropdown from a list:
+
+```js title="script.js"
+const select = document.getElementById("size");
+
+const sizes = [
+ { value: "s", text: "Small" },
+ { value: "m", text: "Medium" },
+ { value: "l", text: "Large" },
+];
+
+select.length = 0; // clear the old options
+select.add(new Option("Choose a size", "")); // the placeholder first
+
+for (const size of sizes) {
+ select.add(new Option(size.text, size.value));
+}
+```
+
+You can also make an `` with `document.createElement("optgroup")`, set its `label`, and append options to it.
+
+Useful properties and tips:
+
+| Property | What it gives |
+| :-- | :-- |
+| `select.options` | A live list of all options (inside groups too) |
+| `select.selectedOptions` | A list of the chosen options |
+| `option.selected` | `true` if chosen now |
+| `option.defaultSelected` | `true` if the HTML has `selected` |
+| `option.disabled` | Read or change the disabled state |
+| `option.hidden` | Hides an option from the list, without removing it |
+
+Prefer `option.text` or `new Option(text, value)` over `innerHTML` when the text comes from a visitor or a database. These treat it as **plain text**, so tags in it are harmless.
+
+## Options in a Datalist
+
+`` is also used inside a [``](./datalist), to suggest values for an ` `. There, the attributes work differently: `value` is the suggestion that is **typed into the input**, and `label` is optional extra text. You will study this in the Datalist lesson.
+
+## Styling Options and Groups
+
+You cannot style options very much, because the browser or operating system draws the open list.
+
+```css title="style.css"
+select {
+ font: inherit;
+ padding: 0.6rem 0.75rem;
+}
+
+/* Works in many desktop browsers, ignored in others */
+option {
+ padding: 0.4rem;
+}
+
+option:disabled {
+ color: #64748b;
+}
+
+optgroup {
+ font-style: normal;
+ font-weight: 700;
+}
+```
+
+- Colours, fonts, and padding on `` work in some browsers (usually on desktop) and are ignored in others, such as on iOS, where a native wheel picker is used.
+- You **cannot** put images, icons, or rich formatting inside an option in a standard select.
+- Never rely on styling for meaning. A disabled option must also say why, in text.
+- A newer feature, `appearance: base-select`, lets supporting browsers style the whole list, and allows richer content in options. Treat it as an **enhancement** only, with a plain select as the fallback.
+- If you truly need rich items (flags, avatars, two-line descriptions), consider a different control: radio buttons shown as cards, or a well-built accessible "listbox." Do this only with great care, since the native select gives you the keyboard, screen readers, and phone pickers for free.
+
+## Accessibility at a Glance
+
+- **Use ` ` for long lists.** Screen readers announce the group name as you move into it, which helps people understand where they are.
+- **Keep option text short and clear,** with the most important word first. Keyboard type-ahead jumps to the option that starts with the typed letters.
+- **Do not repeat the group name in each option.** "HTML," not "Web development: HTML."
+- **Explain disabled options** in their text, since the reason cannot be reached otherwise.
+- **Order options logically** (alphabetical, by size, by date), and keep common choices easy to find.
+- **Keep a placeholder option** ("Choose a size") so the visitor makes a real choice, and the select is not read with an arbitrary first option.
+- **Avoid options that look like headings** (for example, `--- Europe --- `). Use ``, which is announced correctly.
+- **Do not use an option only for decoration,** such as a row of dashes, a blank spacer, or an emoji with no word.
+- **Make sure text works without colour,** and that any symbols in option text (such as a tick) are also described in words.
+
+## Security and Validation Notes
+
+- **Only the `value` is sent,** and anyone can change it. **Allow-list** the values on the server, and reject anything else, even if it looks like a disabled option.
+- **Never trust a hidden meaning** in a value, such as a price or a discount. Look it up on the server.
+- **Escape option text** that comes from a database or a user when you build HTML on the server. Use `new Option()` or `textContent` in JavaScript.
+- **Check that the chosen option is still available** when the form is submitted. A seat or product may have sold out after the page loaded.
+
+See [Secure Form Design](./secure-form-design).
+
+## Global Audience Notes
+
+- **Translate the text, keep the values.** Option text is for people, so translate it. Option values are for your server, so keep them as stable codes.
+- **Sort in the visitor's language.** Alphabetical order differs between languages. In JavaScript, use `Intl.Collator`, or `localeCompare` with the language.
+- **Use standard codes** for values: ISO 3166 for countries, ISO 639 for languages, ISO 4217 for currencies.
+- **Group names should translate well.** Keep them short and plain.
+- **Be inclusive with the options you offer.** Lists for gender, title, or relationship need care, plus "Other" or "Prefer not to say" where appropriate. Ask only for what you truly need.
+- **Right-to-left pages** mirror the list automatically with `dir="rtl"`, including the group headings.
+
+## Common Mistakes to Avoid
+
+1. **Leaving out `value`,** so the (possibly translated) text is sent.
+2. **A disabled placeholder without `selected`,** so the browser picks the first enabled option.
+3. **Putting HTML (bold, links, images) inside an option.** Only text works.
+4. **Nesting `` inside ``.** It is not allowed.
+5. **Forgetting the `label` on an ``.**
+6. **Faking groups** with disabled options and dashes instead of ``.
+7. **Giving duplicate values** to different options, so the server cannot tell them apart.
+8. **Disabling options without saying why.**
+9. **Trusting that a disabled option cannot be sent.** It can, by hand, so check on the server.
+10. **Using `option.innerHTML`** with untrusted text.
+11. **Using several `selected` options** in a single-choice select (only the last counts).
+12. **Relying on `label` or styling** that some browsers ignore.
+
+## Practice Time
+
+Try these in your editor:
+
+1. Build a select with three options and `value`s. Submit it with `method="get"`.
+2. Remove the `value`s, and see what is sent instead.
+3. Add `selected` to the second option, and then to two options, and see which wins.
+4. Add a disabled placeholder as the first option, with and without `selected`.
+5. Disable one option, and try to choose it with the mouse and the keyboard.
+6. Group the options with two ``s, and add a disabled group.
+7. Try to nest an `` in another, and see what the browser does.
+8. Log `option.value`, `option.text`, `option.label`, and `option.index` when the choice changes.
+9. Fill a select from an array with `new Option(text, value)`.
+10. Bonus: use `selectedOptions` on a `multiple` select, and print all the chosen values.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What is the difference between an option's text and its value?
+
+The text between the tags is what the visitor reads. The `value` attribute is what is sent to the server. If `value` is missing, the text is used as the value, so give options stable values.
+
+
+
+
+2. Which option does a single-choice select start with?
+
+The one with `selected` (the last, if several have it). If none does, the first option that is **not disabled**. That is why a disabled placeholder also needs `selected`.
+
+
+
+
+3. What happens to a disabled option?
+
+It is greyed out, cannot be chosen, and is not sent by a normal visitor. The server must still check, since anyone can send that value by hand.
+
+
+
+
+4. What does `` do, and what are its rules?
+
+It groups related options under a heading given by its required `label`. It cannot be nested, the heading cannot be chosen, `disabled` on the group disables every option inside, and only the option's value (never the group label) is sent.
+
+
+
+
+5. How do you add an option with JavaScript?
+
+Create it with `new Option(text, value)` and add it with `select.add(option)`. Remove options with `select.remove(index)`, or empty the list with `select.length = 0`.
+
+
+
+
+6. Can you put an image or bold text inside an option?
+
+No. Standard options hold plain text only. For rich items, use another control, or a carefully built accessible listbox, with a plain select as a fallback.
+
+
+
+## Key Takeaways
+
+- `` is one choice: its **text** is what people read, and its **value** is what is sent. Always give stable values.
+- `selected` sets the starting choice. With none, the browser picks the first non-disabled option, so a disabled placeholder also needs `selected`.
+- `disabled` greys out an option or a whole ` `, and is not sent, but the server must still check values against an allow-list.
+- `` gives long lists headings. It allows one level only, and the group label is never sent.
+- Use `new Option(text, value)` and `select.add()` to build lists in JavaScript, and prefer text over `innerHTML`.
+- Options are plain text with limited styling. Keep text short, translate it, keep the values as codes, and explain disabled choices.
+
+Dropdowns suggest a fixed list. Next, learn how to suggest values while still letting people type their own, in [Datalist](./datalist), or return to the [Forms overview](/tutorials/html/forms).
\ No newline at end of file
diff --git a/tutorials/html/08-forms/pattern.mdx b/tutorials/html/08-forms/pattern.mdx
index b7cb87cc1..15431acbb 100644
--- a/tutorials/html/08-forms/pattern.mdx
+++ b/tutorials/html/08-forms/pattern.mdx
@@ -1,16 +1,526 @@
---
-id: pattern
-title: "Custom Text Validation: The pattern Attribute and Regular Expressions"
-sidebar_label: "Pattern Attribute (RegEx)"
-sidebar_position: 13
+id: html-pattern
+title: "HTML Pattern Attribute"
+sidebar_label: "Pattern"
+sidebar_position: 14
slug: /html/forms/pattern
-description: "Enforce exact string formats like zip codes or custom IDs using the pattern attribute paired with regular expressions (RegEx)."
+description: "Learn the HTML pattern attribute: how regular expressions validate text fields, which input types support it, the implicit anchoring rule, writing helpful title messages, common patterns for codes and usernames, why strict patterns for names and phone numbers fail global visitors, the patternMismatch validity flag, and why the server must validate again."
keywords:
- HTML pattern attribute
- - regex form validation
- - regular expression input
- - custom field format HTML
+ - pattern regex input
+ - HTML5 input pattern
+ - patternMismatch
+ - pattern title attribute
+ - regular expression form validation
+ - pattern unicode names
+ - pattern postal code
+ - HTML form validation pattern
+ - learn HTML forms
- CodeHarborHub HTML
-authors: [ajay-dhangar]
-tags: [html, forms, validation, regex]
----
\ No newline at end of file
+tags: [html, web-development, beginner, forms, validation, pattern, regex, frontend]
+---
+
+import {useState} from 'react';
+
+export function compile(source) {
+ try {
+ return new RegExp("^(?:" + source + ")$", "v");
+ } catch (first) {
+ try {
+ return new RegExp("^(?:" + source + ")$", "u");
+ } catch (second) {
+ return null;
+ }
+ }
+}
+
+export function PatternDemo() {
+ const presets = [
+ {
+ label: "Product code",
+ pattern: "[A-Z]{3}-[0-9]{4}",
+ title: "Three capital letters, a hyphen, and four digits, like ABC-1234",
+ example: "ABC-1234",
+ },
+ {
+ label: "Username",
+ pattern: "[a-z0-9_]{3,16}",
+ title: "3 to 16 characters: lowercase letters, digits, or underscores",
+ example: "ada_99",
+ },
+ {
+ label: "Five-digit ZIP code",
+ pattern: "[0-9]{5}",
+ title: "Five digits, like 90210",
+ example: "90210",
+ },
+ {
+ label: "Hex colour",
+ pattern: "#[0-9a-fA-F]{6}",
+ title: "A hash and six hex digits, like #1a2b3c",
+ example: "#1a2b3c",
+ },
+ {
+ label: "Not only spaces",
+ pattern: ".*\\S.*",
+ title: "Enter at least one character that is not a space",
+ example: "hello",
+ },
+ ];
+ const [index, setIndex] = useState(0);
+ const [value, setValue] = useState("");
+ const preset = presets[index];
+ const regex = compile(preset.pattern);
+ let verdict = "Empty: pattern is skipped for empty fields (add required to demand a value).";
+ let color = "#64748b";
+ if (value !== "") {
+ if (regex && regex.test(value)) {
+ verdict = "Matches. patternMismatch is false.";
+ color = "#15803d";
+ } else {
+ verdict = "No match. patternMismatch is true.";
+ color = "#b91c1c";
+ }
+ }
+ return (
+ event.preventDefault()}>
+
+