مكونات واجهة المستخدم متوسط
RTL Form
Bidirectional form with validation and labels that properly aligns for both LTR and RTL languages using CSS logical properties.
فتح في المختبر
MCP
css vanilla-js
الأهداف: JS HTML
الكود
/* ── Reset & Base ── */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0a0a0a;
--bg-card: #111111;
--bg-input: #181818;
--border: #252525;
--border-focus: #3b82f6;
--border-error: #ef4444;
--border-success: #22c55e;
--text-primary: #f0f0f0;
--text-secondary: #999999;
--text-muted: #666666;
--accent-blue: #3b82f6;
--accent-green: #22c55e;
--accent-amber: #f59e0b;
--accent-red: #ef4444;
--radius: 10px;
}
html {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
}
body {
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding-block: 40px;
padding-inline: 20px;
}
button {
font: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* ── Page ── */
.page {
inline-size: 100%;
max-inline-size: 560px;
}
/* ── Toolbar ── */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-block-end: 32px;
}
.toolbar-title {
font-size: 1.5rem;
font-weight: 700;
}
.dir-toggle {
display: flex;
align-items: center;
gap: 8px;
padding-block: 6px;
padding-inline: 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
color: var(--accent-blue);
transition: background 0.2s;
}
.dir-toggle:hover {
background: var(--bg-card);
}
/* ── Form ── */
.form {
display: flex;
flex-direction: column;
gap: 20px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding-block: 32px;
padding-inline: 32px;
}
.form-row {
display: flex;
gap: 16px;
}
.form-row.two-col > .field {
flex: 1;
}
/* ── Field ── */
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
}
.input-wrap {
position: relative;
display: flex;
align-items: center;
}
.input-icon {
position: absolute;
inset-inline-start: 12px;
font-size: 1rem;
color: var(--text-muted);
pointer-events: none;
z-index: 1;
}
.input {
inline-size: 100%;
padding-block: 10px;
padding-inline-start: 40px;
padding-inline-end: 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font: inherit;
font-size: 0.9rem;
transition: border-color 0.2s;
outline: none;
}
.input:focus {
border-color: var(--border-focus);
}
.input.error {
border-color: var(--border-error);
}
.input.valid {
border-color: var(--border-success);
}
.input::placeholder {
color: var(--text-muted);
}
/* Select */
.select-wrap {
position: relative;
}
.select {
appearance: none;
cursor: pointer;
}
.select-arrow {
position: absolute;
inset-inline-end: 14px;
font-size: 0.8rem;
color: var(--text-muted);
pointer-events: none;
}
/* Textarea */
.textarea {
padding-inline-start: 14px;
resize: vertical;
min-block-size: 80px;
}
.char-count {
font-size: 0.75rem;
color: var(--text-muted);
text-align: end;
}
/* Password toggle */
.toggle-pass {
position: absolute;
inset-inline-end: 10px;
font-size: 1rem;
padding: 4px;
color: var(--text-muted);
}
.toggle-pass:hover {
color: var(--text-secondary);
}
/* Password strength */
.password-strength {
block-size: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.strength-bar {
block-size: 100%;
inline-size: 0;
border-radius: 2px;
transition: inline-size 0.3s, background 0.3s;
}
/* Error message */
.error-msg {
font-size: 0.78rem;
color: var(--accent-red);
min-block-size: 1.2em;
}
/* Checkbox */
.checkbox-field {
margin-block-start: 4px;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-secondary);
}
.checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.checkmark {
flex-shrink: 0;
inline-size: 20px;
block-size: 20px;
border: 2px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
margin-block-start: 1px;
}
.checkbox:checked + .checkmark {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.checkbox:checked + .checkmark::after {
content: "\2713";
color: #fff;
font-size: 0.7rem;
font-weight: 700;
}
.checkbox:focus-visible + .checkmark {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
}
.link {
color: var(--accent-blue);
}
/* Submit */
.submit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding-block: 12px;
padding-inline: 24px;
background: var(--accent-blue);
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
color: #fff;
transition: background 0.2s;
margin-block-start: 8px;
}
.submit-btn:hover {
background: #2563eb;
}
.submit-btn:active {
transform: scale(0.98);
}
[dir="rtl"] .btn-arrow {
transform: scaleX(-1);
}
/* Success */
.success-msg {
text-align: center;
padding-block: 48px;
padding-inline: 32px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-block-start: 24px;
}
.success-icon {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 56px;
block-size: 56px;
border-radius: 50%;
background: rgba(34, 197, 94, 0.15);
color: var(--accent-green);
font-size: 1.6rem;
margin-block-end: 16px;
}
.success-title {
font-size: 1.2rem;
font-weight: 600;
margin-block-end: 8px;
}
.success-text {
font-size: 0.9rem;
color: var(--text-secondary);
}
/* ── Responsive ── */
@media (max-width: 600px) {
.form-row.two-col {
flex-direction: column;
}
.form {
padding-block: 24px;
padding-inline: 20px;
}
}(() => {
const html = document.documentElement;
const dirToggle = document.getElementById("dir-toggle");
const dirLabel = document.getElementById("dir-label");
const form = document.getElementById("reg-form");
const successMsg = document.getElementById("success-msg");
const togglePass = document.getElementById("toggle-pass");
const passwordInput = document.getElementById("password");
const strengthBar = document.getElementById("strength-bar");
const bioInput = document.getElementById("bio");
const bioCount = document.getElementById("bio-count");
/* ── Direction toggle ── */
dirToggle.addEventListener("click", () => {
const isRtl = html.getAttribute("dir") === "rtl";
const newDir = isRtl ? "ltr" : "rtl";
html.setAttribute("dir", newDir);
html.setAttribute("lang", isRtl ? "en" : "ar");
dirLabel.textContent = newDir.toUpperCase();
});
/* ── Password visibility ── */
togglePass.addEventListener("click", () => {
const isPassword = passwordInput.type === "password";
passwordInput.type = isPassword ? "text" : "password";
togglePass.textContent = isPassword ? "\u{1F648}" : "\u{1F441}";
});
/* ── Password strength ── */
passwordInput.addEventListener("input", () => {
const val = passwordInput.value;
let score = 0;
if (val.length >= 8) score++;
if (/[A-Z]/.test(val)) score++;
if (/[0-9]/.test(val)) score++;
if (/[^A-Za-z0-9]/.test(val)) score++;
const pct = (score / 4) * 100;
const colors = ["#ef4444", "#f59e0b", "#f59e0b", "#22c55e"];
strengthBar.style.inlineSize = pct + "%";
strengthBar.style.background = score > 0 ? colors[score - 1] : "transparent";
});
/* ── Bio char count ── */
bioInput.addEventListener("input", () => {
const len = bioInput.value.length;
bioCount.textContent = len;
if (len > 200) {
bioInput.value = bioInput.value.slice(0, 200);
bioCount.textContent = "200";
}
});
/* ── Validation ── */
const rules = {
"first-name": {
validate: (v) => v.trim().length >= 2,
msg: "First name must be at least 2 characters.",
},
"last-name": {
validate: (v) => v.trim().length >= 2,
msg: "Last name must be at least 2 characters.",
},
email: {
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
msg: "Please enter a valid email address.",
},
password: {
validate: (v) => v.length >= 8,
msg: "Password must be at least 8 characters.",
},
country: {
validate: (v) => v !== "",
msg: "Please select a country.",
},
};
function validateField(id) {
const input = document.getElementById(id);
const errEl = document.getElementById(id + "-err");
const rule = rules[id];
if (!rule || !input || !errEl) return true;
const valid = rule.validate(input.value);
input.classList.toggle("error", !valid);
input.classList.toggle("valid", valid);
errEl.textContent = valid ? "" : rule.msg;
return valid;
}
/* Live validation on blur */
Object.keys(rules).forEach((id) => {
const input = document.getElementById(id);
if (input) {
input.addEventListener("blur", () => validateField(id));
input.addEventListener("input", () => {
if (input.classList.contains("error")) validateField(id);
});
}
});
/* ── Submit ── */
form.addEventListener("submit", (e) => {
e.preventDefault();
let allValid = true;
Object.keys(rules).forEach((id) => {
if (!validateField(id)) allValid = false;
});
/* Check terms */
const terms = document.getElementById("terms");
const termsErr = document.getElementById("terms-err");
if (!terms.checked) {
termsErr.textContent = "You must agree to the terms.";
allValid = false;
} else {
termsErr.textContent = "";
}
if (allValid) {
form.hidden = true;
successMsg.hidden = false;
}
});
})();<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RTL Form</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- Direction toggle -->
<div class="toolbar">
<h1 class="toolbar-title">Account Registration</h1>
<button class="dir-toggle" id="dir-toggle" type="button" aria-label="Toggle text direction">
<span id="dir-label">LTR</span>
<span class="dir-icon">⇄</span>
</button>
</div>
<!-- Form -->
<form class="form" id="reg-form" novalidate>
<!-- Name row -->
<div class="form-row two-col">
<div class="field" data-field="firstName">
<label class="label" for="first-name">First Name</label>
<div class="input-wrap">
<span class="input-icon">👤</span>
<input type="text" id="first-name" class="input" placeholder="Ahmed" required minlength="2" />
</div>
<span class="error-msg" id="first-name-err"></span>
</div>
<div class="field" data-field="lastName">
<label class="label" for="last-name">Last Name</label>
<div class="input-wrap">
<span class="input-icon">👤</span>
<input type="text" id="last-name" class="input" placeholder="Hassan" required minlength="2" />
</div>
<span class="error-msg" id="last-name-err"></span>
</div>
</div>
<!-- Email -->
<div class="field" data-field="email">
<label class="label" for="email">Email Address</label>
<div class="input-wrap">
<span class="input-icon">✉</span>
<input type="email" id="email" class="input" placeholder="[email protected]" required />
</div>
<span class="error-msg" id="email-err"></span>
</div>
<!-- Password -->
<div class="field" data-field="password">
<label class="label" for="password">Password</label>
<div class="input-wrap">
<span class="input-icon">🔒</span>
<input type="password" id="password" class="input" placeholder="Minimum 8 characters" required minlength="8" />
<button type="button" class="toggle-pass" id="toggle-pass" aria-label="Show password">👁</button>
</div>
<span class="error-msg" id="password-err"></span>
<div class="password-strength" id="pw-strength">
<div class="strength-bar" id="strength-bar"></div>
</div>
</div>
<!-- Country select -->
<div class="field" data-field="country">
<label class="label" for="country">Country</label>
<div class="input-wrap select-wrap">
<span class="input-icon">🌎</span>
<select id="country" class="input select" required>
<option value="">Select a country...</option>
<option value="SA">Saudi Arabia</option>
<option value="AE">United Arab Emirates</option>
<option value="EG">Egypt</option>
<option value="JO">Jordan</option>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="DE">Germany</option>
<option value="FR">France</option>
</select>
<span class="select-arrow">▾</span>
</div>
<span class="error-msg" id="country-err"></span>
</div>
<!-- Bio textarea -->
<div class="field" data-field="bio">
<label class="label" for="bio">Bio</label>
<textarea id="bio" class="input textarea" placeholder="Tell us about yourself..." rows="4"></textarea>
<div class="char-count"><span id="bio-count">0</span> / 200</div>
</div>
<!-- Terms checkbox -->
<div class="field checkbox-field">
<label class="checkbox-label">
<input type="checkbox" id="terms" class="checkbox" required />
<span class="checkmark"></span>
<span class="checkbox-text">I agree to the <a href="#" class="link">Terms of Service</a> and <a href="#" class="link">Privacy Policy</a></span>
</label>
<span class="error-msg" id="terms-err"></span>
</div>
<!-- Submit -->
<button type="submit" class="submit-btn" id="submit-btn">
<span class="btn-text">Create Account</span>
<span class="btn-arrow">→</span>
</button>
</form>
<!-- Success message -->
<div class="success-msg" id="success-msg" hidden>
<span class="success-icon">✓</span>
<h2 class="success-title">Account Created!</h2>
<p class="success-text">Your account has been created successfully.</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>A bidirectional form with inline validation that adapts its layout, labels, icons, and error messages for both LTR and RTL languages using CSS logical properties throughout.