UI Components Medium
RTL Form
Bidirectional form with validation and labels that properly aligns for both LTR and RTL languages using CSS logical properties.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
/* ── 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="ahmed@example.com" 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.