Day 15: Styling Forms & Tables
✅ Today you will learn
- Consistent inputs: padding, borders, radius, font: inherit
- Focus rings that are visible but beautiful
- Validation colours with :user-invalid / :user-valid and :has()
- Custom select arrow, pill radios, accent-color checkboxes
- Data tables: zebra rows, sticky header, responsive card layout on phones
🏢 In the real world
Forms are where a business earns money – orders, admissions, enquiries. A default browser form looks different on every phone, shows errors in red before the user has typed anything, and the order table of the admin panel overflows on mobile. Today you fix all of that for the Sharma Sweets order form and the admin orders table.
A complete, styled order form
<style>
:root { --brand: #264de4; --ok: #15803d; --err: #b91c1c; --line: #cbd5e1; }
* { box-sizing: border-box; }
.form { max-width: 440px; background: #fff; padding: 20px; border-radius: 16px; box-shadow: 0 10px 30px rgb(0 0 0 / 8%); display: grid; gap: 14px; }
.field label { display: block; font-weight: 600; margin-bottom: 4px; }
.field input, .field select {
width: 100%; padding: 10px 12px; font: inherit;
border: 1.5px solid var(--line); border-radius: 10px; background: #fff;
transition: border-color .2s, box-shadow .2s;
}
.field input:focus, .field select:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px rgb(38 77 228 / 15%); }
.field input::placeholder { color: #94a3b8; }
/* validation only after the user has typed (not on page load) */
.field input:user-invalid { border-color: var(--err); }
.field input:user-valid { border-color: var(--ok); }
.hint { font-size: .8rem; color: #64748b; }
.field:has(input:user-invalid) .hint { color: var(--err); }
.field select { appearance: none; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23475569' stroke-width='2' fill='none'/%3E%3C/svg%3E") no-repeat right 12px center; padding-right: 34px; }
.choices { display: flex; gap: 10px; flex-wrap: wrap; }
.choice input { position: absolute; opacity: 0; }
.choice span { display: inline-block; padding: 8px 14px; border: 1.5px solid var(--line); border-radius: 99px; cursor: pointer; }
.choice input:checked + span { background: var(--brand); border-color: var(--brand); color: #fff; }
.choice input:focus-visible + span { outline: 3px solid #f59e0b; outline-offset: 2px; }
.check { display: flex; gap: 8px; align-items: center; }
.check input { width: 20px; height: 20px; accent-color: var(--brand); }
.submit { background: var(--brand); color: #fff; border: 0; padding: 12px; border-radius: 10px; font: 600 1rem system-ui; cursor: pointer; }
.submit:disabled { opacity: .5; cursor: not-allowed; }
</style>
<form class="form" onsubmit="alert('Valid! Order sent.'); return false">
<div class="field">
<label for="n">Full name</label>
<input id="n" required minlength="2" placeholder="e.g. Aarti Gupta" autocomplete="name">
</div>
<div class="field">
<label for="m">Mobile</label>
<input id="m" type="tel" required pattern="[6-9][0-9]{9}" placeholder="10-digit mobile" aria-describedby="mh">
<div class="hint" id="mh">Starts with 6–9, 10 digits</div>
</div>
<div class="field">
<label for="s">Sweet</label>
<select id="s" required><option value="">Choose…</option><option>Kaju katli</option><option>Ghewar</option></select>
</div>
<fieldset class="choices" style="border:0;padding:0;margin:0">
<legend style="font-weight:600;margin-bottom:6px">Box size</legend>
<label class="choice"><input type="radio" name="b" checked><span>500 g</span></label>
<label class="choice"><input type="radio" name="b"><span>1 kg</span></label>
<label class="choice"><input type="radio" name="b"><span>2 kg</span></label>
</fieldset>
<label class="check"><input type="checkbox" required> Gift wrap with a card</label>
<button class="submit">Place order</button>
</form>| Technique | Why |
|---|---|
| font: inherit on inputs | Browsers use a small system font in inputs by default |
| padding ≥ 10px, height ≥ 44px | Easy to tap on phones |
| :focus with box-shadow ring | Visible, on-brand focus for keyboard users |
| :user-invalid / :user-valid | Show errors only after the user interacts – not on page load |
| .field:has(input:user-invalid) .hint | Turn the hint red without JavaScript |
| appearance: none + SVG arrow | Same select design on every device |
| Hidden radio + styled span | Pill buttons that stay keyboard-accessible |
| accent-color | Brand-coloured checkboxes in one line |
outline: none alone makes the form unusable with a keyboard. Always pair it with a visible alternative such as the box-shadow ring above.
:invalid matches immediately, so an empty required field is red as soon as the page opens – it feels like scolding the user. :user-invalid waits until the user has typed and left the field or tried to submit.
Data tables that stay readable
<style>
.tbl-wrap { max-height: 230px; overflow: auto; border: 1px solid #e2e8f0; border-radius: 12px; }
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #f1f5f9; }
thead th { position: sticky; top: 0; background: #f8fafc; color: #475569; font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; z-index: 1; }
tbody tr:nth-child(even) { background: #fafafa; }
tbody tr:hover { background: #eef2ff; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.st { padding: 2px 10px; border-radius: 99px; font-size: .75rem; font-weight: 700; }
.st.done { background: #dcfce7; color: #166534; } .st.pack { background: #fef3c7; color: #92400e; } .st.new { background: #dbeafe; color: #1e40af; }
</style>
<div class="tbl-wrap"><table>
<thead><tr><th>Order</th><th>Customer</th><th>Items</th><th class="num">Amount</th><th>Status</th></tr></thead>
<tbody>
<tr><td>#1042</td><td>Aarti Gupta</td><td>Kaju katli 2 kg</td><td class="num">₹1,800</td><td><span class="st done">Delivered</span></td></tr>
<tr><td>#1043</td><td>R. Meena</td><td>Ghewar 1 kg</td><td class="num">₹650</td><td><span class="st pack">Packing</span></td></tr>
<tr><td>#1044</td><td>S. Khan</td><td>Assorted box</td><td class="num">₹720</td><td><span class="st new">New</span></td></tr>
<tr><td>#1045</td><td>P. Jain</td><td>Rasmalai 3 kg</td><td class="num">₹2,100</td><td><span class="st new">New</span></td></tr>
<tr><td>#1046</td><td>V. Singh</td><td>Ladoo 5 kg</td><td class="num">₹2,500</td><td><span class="st pack">Packing</span></td></tr>
<tr><td>#1047</td><td>M. Bansal</td><td>Kaju katli 500 g</td><td class="num">₹450</td><td><span class="st done">Delivered</span></td></tr>
</tbody>
</table></div>stack
<style>
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #e2e8f0; }
@media (max-width: 520px) {
thead { position: absolute; left: -9999px; } /* hide visually, keep for screen readers */
tr { display: block; margin-bottom: 10px; border: 1px solid #e2e8f0; border-radius: 10px; padding: 6px; }
td { display: flex; justify-content: space-between; border: 0; padding: 4px 6px; }
td::before { content: attr(data-label); font-weight: 600; color: #64748b; }
}
</style>
<table>
<thead><tr><th>Order</th><th>Customer</th><th>Amount</th></tr></thead>
<tbody>
<tr><td data-label="Order">#1042</td><td data-label="Customer">Aarti Gupta</td><td data-label="Amount">₹1,800</td></tr>
<tr><td data-label="Order">#1043</td><td data-label="Customer">R. Meena</td><td data-label="Amount">₹650</td></tr>
</tbody>
</table>
<p style="font:13px system-ui;color:#64748b">Narrow the window below 520px to see the card layout.</p>Option A: scroll wrapper
- overflow-x: auto around the table
- Keeps the table shape
- Best for many numeric columns (reports)
Option B: stacked cards
- Rows become labelled cards
- No sideways scrolling
- Best for 3–6 columns (orders, students)
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How do you style a custom radio button accessibly?
Keep the real input in the DOM (visually hidden, not display: none) so it stays focusable and announced; style an adjacent element using :checked and :focus-visible selectors; wrap both in a label for a large click target.
How do you make a wide table responsive?
Either wrap it in an overflow-x: auto container (keeps comparison ability) or, for few columns, switch rows to stacked cards in a media query using data-label attributes with ::before. Keep a real thead for screen readers.
Summary
- Inputs: font: inherit, generous padding, radius, clear focus ring.
- :user-invalid / :user-valid + :has() for friendly validation.
- appearance: none, hidden radios and accent-color for custom controls.
- Tables: sticky head, zebra, tabular numbers; scroll or stack on phones.
Day 16: a reusable UI component kit – buttons, badges, cards, alerts, tabs, modal and toast.
