20 CSS Form Validation17 / 20
Newsletter Email Pattern Validation
A single email field inline with its button, and an honest account of what type="email" does and does not check. The native type accepts a@b — no dot required — so a pattern tightens it to something with a real domain. The demo also argues the other side: over-strict email regexes reject plus-addressing, long new TLDs and unicode local parts, and those rejections cost you real subscribers.
Published
The code
<section class="fv-17" aria-labelledby="fv-17-h">
<fieldset class="fv-17__theme">
<legend class="fv-17__themelegend">Theme</legend>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-auto" checked>
<label class="fv-17__themeopt" for="fv-17-theme-auto">Auto</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-light">
<label class="fv-17__themeopt" for="fv-17-theme-light">Light</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-dark">
<label class="fv-17__themeopt" for="fv-17-theme-dark">Dark</label>
</fieldset>
<div class="fv-17__stage">
<div class="fv-17__col">
<p class="fv-17__kicker">The Vale Dispatch · Thursdays</p>
<h2 class="fv-17__h" id="fv-17-h">One field, two arguments about it</h2>
<p class="fv-17__lede">The native email type will happily accept <code>alex@b</code>. A pattern fixes that — and a stricter one starts throwing away real subscribers.</p>
<form class="fv-17__form" aria-labelledby="fv-17-h">
<label class="fv-17__label" for="fv-17-email">Email address</label>
<div class="fv-17__unit">
<input class="fv-17__input" id="fv-17-email" name="fv-17-email" type="email" required autocomplete="email" pattern="[^@\s]+@[^@\s]+\.[a-zA-Z][a-zA-Z]+" placeholder="alex@vale.co" value="alex@vale" title="Include a domain with a dot, like vale.co" aria-describedby="fv-17-msg">
<button class="fv-17__btn" type="button">Subscribe</button>
</div>
<p class="fv-17__msg" id="fv-17-msg">
<svg class="fv-17__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="1.7"></circle><path d="M10 5.9v5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path><circle cx="10" cy="14.2" r="1.05" fill="currentColor"></circle></svg>
<span>Include a domain with a dot, like alex@vale.co — the address as typed has no top-level domain</span>
</p>
<p class="fv-17__ok">Thanks. Confirm the address from the email we just sent.</p>
</form>
<div class="fv-17__table">
<div class="fv-17__trow fv-17__trow--head"><span>Address</span><span>This pattern</span><span>An over-strict one</span></div>
<div class="fv-17__trow"><span><code>priya+dispatch@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — plus sign</span></div>
<div class="fv-17__trow"><span><code>jordan@studio.design</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — long TLD</span></div>
<div class="fv-17__trow"><span><code>sam@vale</code></span><span data-v="fail">Rejected — no dot</span><span data-v="fail">Rejected</span></div>
<div class="fv-17__trow"><span><code>alex@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="pass">Accepted</span></div>
</div>
</div>
</div>
</section><section class="fv-17" aria-labelledby="fv-17-h">
<fieldset class="fv-17__theme">
<legend class="fv-17__themelegend">Theme</legend>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-auto" checked>
<label class="fv-17__themeopt" for="fv-17-theme-auto">Auto</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-light">
<label class="fv-17__themeopt" for="fv-17-theme-light">Light</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-dark">
<label class="fv-17__themeopt" for="fv-17-theme-dark">Dark</label>
</fieldset>
<div class="fv-17__stage">
<div class="fv-17__col">
<p class="fv-17__kicker">The Vale Dispatch · Thursdays</p>
<h2 class="fv-17__h" id="fv-17-h">One field, two arguments about it</h2>
<p class="fv-17__lede">The native email type will happily accept <code>alex@b</code>. A pattern fixes that — and a stricter one starts throwing away real subscribers.</p>
<form class="fv-17__form" aria-labelledby="fv-17-h">
<label class="fv-17__label" for="fv-17-email">Email address</label>
<div class="fv-17__unit">
<input class="fv-17__input" id="fv-17-email" name="fv-17-email" type="email" required autocomplete="email" pattern="[^@\s]+@[^@\s]+\.[a-zA-Z][a-zA-Z]+" placeholder="alex@vale.co" value="alex@vale" title="Include a domain with a dot, like vale.co" aria-describedby="fv-17-msg">
<button class="fv-17__btn" type="button">Subscribe</button>
</div>
<p class="fv-17__msg" id="fv-17-msg">
<svg class="fv-17__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="1.7"></circle><path d="M10 5.9v5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path><circle cx="10" cy="14.2" r="1.05" fill="currentColor"></circle></svg>
<span>Include a domain with a dot, like alex@vale.co — the address as typed has no top-level domain</span>
</p>
<p class="fv-17__ok">Thanks. Confirm the address from the email we just sent.</p>
</form>
<div class="fv-17__table">
<div class="fv-17__trow fv-17__trow--head"><span>Address</span><span>This pattern</span><span>An over-strict one</span></div>
<div class="fv-17__trow"><span><code>priya+dispatch@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — plus sign</span></div>
<div class="fv-17__trow"><span><code>jordan@studio.design</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — long TLD</span></div>
<div class="fv-17__trow"><span><code>sam@vale</code></span><span data-v="fail">Rejected — no dot</span><span data-v="fail">Rejected</span></div>
<div class="fv-17__trow"><span><code>alex@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="pass">Accepted</span></div>
</div>
</div>
</div>
</section>.fv-17 {
--page: #faf8f4;
--ink: #131210;
--muted: #5d564d;
--rule: #ddd7cc;
--accent: #1a1917;
--error: #a03118;
--error-ink: #872710;
--ok: #2f6140;
--display-size: clamp(1.9rem, 6vw, 3.4rem);
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--page);
color: var(--ink);
font-family: system-ui, sans-serif;
}
@supports (color: oklch(0 0 0)) {
.fv-17 {
--error: oklch(0.46 0.16 35);
--error-ink: oklch(0.39 0.16 35);
--ok: oklch(0.46 0.1 155);
}
}
.fv-17 *,
.fv-17 *::before,
.fv-17 *::after {
box-sizing: border-box;
}
.fv-17__stage {
display: grid;
place-items: center;
min-block-size: 100vh;
max-inline-size: 100%;
padding: clamp(1.75rem, 6vw, 5rem) clamp(1rem, 5vw, 3rem);
}
.fv-17__col {
inline-size: min(100%, 44rem);
min-inline-size: 0;
}
.fv-17__kicker {
margin: 0 0 1rem;
padding-block-end: 1rem;
border-block-end: 1px solid var(--rule);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__h {
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-size: var(--display-size);
font-weight: 400;
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
}
.fv-17__lede {
margin: 0 0 2rem;
max-inline-size: 34rem;
font-size: 1.0625rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.fv-17__lede code,
.fv-17__trow code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.86em;
color: var(--ink);
}
.fv-17__form {
display: grid;
gap: 0.5rem;
min-inline-size: 0;
max-inline-size: 34rem;
}
.fv-17__label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__unit {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: stretch;
min-inline-size: 0;
border: 1px solid var(--ink);
background: transparent;
}
.fv-17__input {
min-inline-size: 0;
inline-size: 100%;
block-size: 3.25rem;
padding: 0 1rem;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: transparent;
border: 0;
}
.fv-17__input::placeholder {
color: #a29a8d;
}
.fv-17__input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.fv-17__btn {
min-block-size: 3.25rem;
min-inline-size: 44px;
padding: 0 1.5rem;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--page);
background: var(--accent);
border: 0;
cursor: pointer;
}
.fv-17__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.fv-17__msg,
.fv-17__ok {
display: none;
gap: 0.4375rem;
align-items: flex-start;
margin: 0;
min-inline-size: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.fv-17__msg {
color: var(--error-ink);
}
.fv-17__ok {
color: var(--ok);
font-weight: 600;
}
.fv-17__msg span {
min-inline-size: 0;
}
.fv-17__icon {
flex: none;
inline-size: 1.0625rem;
block-size: 1.0625rem;
margin-block-start: 0.12rem;
}
.fv-17__unit:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) {
border-color: var(--error);
}
.fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) .fv-17__msg {
display: flex;
}
.fv-17__unit:has(.fv-17__input:user-invalid) {
border-color: var(--error);
}
.fv-17__input:user-invalid ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:user-invalid) .fv-17__msg {
display: flex;
}
.fv-17__form:has(.fv-17__input:user-valid) .fv-17__ok {
display: flex;
}
.fv-17__table {
display: grid;
gap: 0;
margin-block-start: 2.5rem;
min-inline-size: 0;
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem 1rem;
padding-block: 0.75rem;
border-block-end: 1px solid var(--rule);
font-size: 0.8125rem;
line-height: 1.45;
min-inline-size: 0;
}
.fv-17__trow span {
min-inline-size: 0;
}
.fv-17__trow--head {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__trow [data-v="pass"] {
color: var(--ok);
font-weight: 600;
}
.fv-17__trow [data-v="fail"] {
color: var(--error-ink);
font-weight: 600;
}
@media (max-width: 30rem) {
.fv-17__unit {
grid-template-columns: minmax(0, 1fr);
}
.fv-17__btn {
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
grid-template-columns: minmax(0, 1fr);
gap: 0.25rem;
}
.fv-17__trow--head {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17 {
position: relative;
}
.fv-17__stage {
padding-block-start: 4.75rem;
}
.fv-17__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 6;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 7%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.fv-17__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 1.875rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.fv-17__themeopt:hover {
opacity: 0.85;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.fv-17__themeinput:focus-visible + .fv-17__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (forced-colors: active) {
.fv-17__theme {
border-color: CanvasText;
background: Canvas;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
background: Highlight;
color: HighlightText;
}
}
.fv-17:has(#fv-17-theme-dark:checked) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__btn {
color: #100f0d;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}.fv-17 {
--page: #faf8f4;
--ink: #131210;
--muted: #5d564d;
--rule: #ddd7cc;
--accent: #1a1917;
--error: #a03118;
--error-ink: #872710;
--ok: #2f6140;
--display-size: clamp(1.9rem, 6vw, 3.4rem);
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--page);
color: var(--ink);
font-family: system-ui, sans-serif;
}
@supports (color: oklch(0 0 0)) {
.fv-17 {
--error: oklch(0.46 0.16 35);
--error-ink: oklch(0.39 0.16 35);
--ok: oklch(0.46 0.1 155);
}
}
.fv-17 *,
.fv-17 *::before,
.fv-17 *::after {
box-sizing: border-box;
}
.fv-17__stage {
display: grid;
place-items: center;
min-block-size: 100vh;
max-inline-size: 100%;
padding: clamp(1.75rem, 6vw, 5rem) clamp(1rem, 5vw, 3rem);
}
.fv-17__col {
inline-size: min(100%, 44rem);
min-inline-size: 0;
}
.fv-17__kicker {
margin: 0 0 1rem;
padding-block-end: 1rem;
border-block-end: 1px solid var(--rule);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__h {
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-size: var(--display-size);
font-weight: 400;
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
}
.fv-17__lede {
margin: 0 0 2rem;
max-inline-size: 34rem;
font-size: 1.0625rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.fv-17__lede code,
.fv-17__trow code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.86em;
color: var(--ink);
}
.fv-17__form {
display: grid;
gap: 0.5rem;
min-inline-size: 0;
max-inline-size: 34rem;
}
.fv-17__label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__unit {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: stretch;
min-inline-size: 0;
border: 1px solid var(--ink);
background: transparent;
}
.fv-17__input {
min-inline-size: 0;
inline-size: 100%;
block-size: 3.25rem;
padding: 0 1rem;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: transparent;
border: 0;
}
.fv-17__input::placeholder {
color: #a29a8d;
}
.fv-17__input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.fv-17__btn {
min-block-size: 3.25rem;
min-inline-size: 44px;
padding: 0 1.5rem;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--page);
background: var(--accent);
border: 0;
cursor: pointer;
}
.fv-17__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.fv-17__msg,
.fv-17__ok {
display: none;
gap: 0.4375rem;
align-items: flex-start;
margin: 0;
min-inline-size: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.fv-17__msg {
color: var(--error-ink);
}
.fv-17__ok {
color: var(--ok);
font-weight: 600;
}
.fv-17__msg span {
min-inline-size: 0;
}
.fv-17__icon {
flex: none;
inline-size: 1.0625rem;
block-size: 1.0625rem;
margin-block-start: 0.12rem;
}
.fv-17__unit:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) {
border-color: var(--error);
}
.fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) .fv-17__msg {
display: flex;
}
.fv-17__unit:has(.fv-17__input:user-invalid) {
border-color: var(--error);
}
.fv-17__input:user-invalid ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:user-invalid) .fv-17__msg {
display: flex;
}
.fv-17__form:has(.fv-17__input:user-valid) .fv-17__ok {
display: flex;
}
.fv-17__table {
display: grid;
gap: 0;
margin-block-start: 2.5rem;
min-inline-size: 0;
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem 1rem;
padding-block: 0.75rem;
border-block-end: 1px solid var(--rule);
font-size: 0.8125rem;
line-height: 1.45;
min-inline-size: 0;
}
.fv-17__trow span {
min-inline-size: 0;
}
.fv-17__trow--head {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__trow [data-v="pass"] {
color: var(--ok);
font-weight: 600;
}
.fv-17__trow [data-v="fail"] {
color: var(--error-ink);
font-weight: 600;
}
@media (max-width: 30rem) {
.fv-17__unit {
grid-template-columns: minmax(0, 1fr);
}
.fv-17__btn {
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
grid-template-columns: minmax(0, 1fr);
gap: 0.25rem;
}
.fv-17__trow--head {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17 {
position: relative;
}
.fv-17__stage {
padding-block-start: 4.75rem;
}
.fv-17__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 6;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 7%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.fv-17__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 1.875rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.fv-17__themeopt:hover {
opacity: 0.85;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.fv-17__themeinput:focus-visible + .fv-17__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (forced-colors: active) {
.fv-17__theme {
border-color: CanvasText;
background: Canvas;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
background: Highlight;
color: HighlightText;
}
}
.fv-17:has(#fv-17-theme-dark:checked) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__btn {
color: #100f0d;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}Here's a working CSS Form Validation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Newsletter Email Pattern Validation
Source: https://codefronts.com/components/css-form-validation/newsletter-email-pattern-validation/
A single email field inline with its button, and an honest account of what type="email" does and does not check. The native type accepts a@b — no dot required — so a pattern tightens it to something with a real domain. The demo also argues the other side: over-strict email regexes reject plus-addressing, long new TLDs and unicode local parts, and those rejections cost you real subscribers.
## HTML
```html
<section class="fv-17" aria-labelledby="fv-17-h">
<fieldset class="fv-17__theme">
<legend class="fv-17__themelegend">Theme</legend>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-auto" checked>
<label class="fv-17__themeopt" for="fv-17-theme-auto">Auto</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-light">
<label class="fv-17__themeopt" for="fv-17-theme-light">Light</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-dark">
<label class="fv-17__themeopt" for="fv-17-theme-dark">Dark</label>
</fieldset>
<div class="fv-17__stage">
<div class="fv-17__col">
<p class="fv-17__kicker">The Vale Dispatch · Thursdays</p>
<h2 class="fv-17__h" id="fv-17-h">One field, two arguments about it</h2>
<p class="fv-17__lede">The native email type will happily accept <code>alex@b</code>. A pattern fixes that — and a stricter one starts throwing away real subscribers.</p>
<form class="fv-17__form" aria-labelledby="fv-17-h">
<label class="fv-17__label" for="fv-17-email">Email address</label>
<div class="fv-17__unit">
<input class="fv-17__input" id="fv-17-email" name="fv-17-email" type="email" required autocomplete="email" pattern="[^@\s]+@[^@\s]+\.[a-zA-Z][a-zA-Z]+" placeholder="alex@vale.co" value="alex@vale" title="Include a domain with a dot, like vale.co" aria-describedby="fv-17-msg">
<button class="fv-17__btn" type="button">Subscribe</button>
</div>
<p class="fv-17__msg" id="fv-17-msg">
<svg class="fv-17__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="1.7"></circle><path d="M10 5.9v5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path><circle cx="10" cy="14.2" r="1.05" fill="currentColor"></circle></svg>
<span>Include a domain with a dot, like alex@vale.co — the address as typed has no top-level domain</span>
</p>
<p class="fv-17__ok">Thanks. Confirm the address from the email we just sent.</p>
</form>
<div class="fv-17__table">
<div class="fv-17__trow fv-17__trow--head"><span>Address</span><span>This pattern</span><span>An over-strict one</span></div>
<div class="fv-17__trow"><span><code>priya+dispatch@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — plus sign</span></div>
<div class="fv-17__trow"><span><code>jordan@studio.design</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — long TLD</span></div>
<div class="fv-17__trow"><span><code>sam@vale</code></span><span data-v="fail">Rejected — no dot</span><span data-v="fail">Rejected</span></div>
<div class="fv-17__trow"><span><code>alex@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="pass">Accepted</span></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
.fv-17 {
--page: #faf8f4;
--ink: #131210;
--muted: #5d564d;
--rule: #ddd7cc;
--accent: #1a1917;
--error: #a03118;
--error-ink: #872710;
--ok: #2f6140;
--display-size: clamp(1.9rem, 6vw, 3.4rem);
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--page);
color: var(--ink);
font-family: system-ui, sans-serif;
}
@supports (color: oklch(0 0 0)) {
.fv-17 {
--error: oklch(0.46 0.16 35);
--error-ink: oklch(0.39 0.16 35);
--ok: oklch(0.46 0.1 155);
}
}
.fv-17 *,
.fv-17 *::before,
.fv-17 *::after {
box-sizing: border-box;
}
.fv-17__stage {
display: grid;
place-items: center;
min-block-size: 100vh;
max-inline-size: 100%;
padding: clamp(1.75rem, 6vw, 5rem) clamp(1rem, 5vw, 3rem);
}
.fv-17__col {
inline-size: min(100%, 44rem);
min-inline-size: 0;
}
.fv-17__kicker {
margin: 0 0 1rem;
padding-block-end: 1rem;
border-block-end: 1px solid var(--rule);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__h {
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-size: var(--display-size);
font-weight: 400;
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
}
.fv-17__lede {
margin: 0 0 2rem;
max-inline-size: 34rem;
font-size: 1.0625rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.fv-17__lede code,
.fv-17__trow code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.86em;
color: var(--ink);
}
.fv-17__form {
display: grid;
gap: 0.5rem;
min-inline-size: 0;
max-inline-size: 34rem;
}
.fv-17__label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__unit {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: stretch;
min-inline-size: 0;
border: 1px solid var(--ink);
background: transparent;
}
.fv-17__input {
min-inline-size: 0;
inline-size: 100%;
block-size: 3.25rem;
padding: 0 1rem;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: transparent;
border: 0;
}
.fv-17__input::placeholder {
color: #a29a8d;
}
.fv-17__input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.fv-17__btn {
min-block-size: 3.25rem;
min-inline-size: 44px;
padding: 0 1.5rem;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--page);
background: var(--accent);
border: 0;
cursor: pointer;
}
.fv-17__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.fv-17__msg,
.fv-17__ok {
display: none;
gap: 0.4375rem;
align-items: flex-start;
margin: 0;
min-inline-size: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.fv-17__msg {
color: var(--error-ink);
}
.fv-17__ok {
color: var(--ok);
font-weight: 600;
}
.fv-17__msg span {
min-inline-size: 0;
}
.fv-17__icon {
flex: none;
inline-size: 1.0625rem;
block-size: 1.0625rem;
margin-block-start: 0.12rem;
}
.fv-17__unit:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) {
border-color: var(--error);
}
.fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) .fv-17__msg {
display: flex;
}
.fv-17__unit:has(.fv-17__input:user-invalid) {
border-color: var(--error);
}
.fv-17__input:user-invalid ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:user-invalid) .fv-17__msg {
display: flex;
}
.fv-17__form:has(.fv-17__input:user-valid) .fv-17__ok {
display: flex;
}
.fv-17__table {
display: grid;
gap: 0;
margin-block-start: 2.5rem;
min-inline-size: 0;
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem 1rem;
padding-block: 0.75rem;
border-block-end: 1px solid var(--rule);
font-size: 0.8125rem;
line-height: 1.45;
min-inline-size: 0;
}
.fv-17__trow span {
min-inline-size: 0;
}
.fv-17__trow--head {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__trow [data-v="pass"] {
color: var(--ok);
font-weight: 600;
}
.fv-17__trow [data-v="fail"] {
color: var(--error-ink);
font-weight: 600;
}
@media (max-width: 30rem) {
.fv-17__unit {
grid-template-columns: minmax(0, 1fr);
}
.fv-17__btn {
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
grid-template-columns: minmax(0, 1fr);
gap: 0.25rem;
}
.fv-17__trow--head {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17 {
position: relative;
}
.fv-17__stage {
padding-block-start: 4.75rem;
}
.fv-17__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 6;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 7%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.fv-17__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 1.875rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.fv-17__themeopt:hover {
opacity: 0.85;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.fv-17__themeinput:focus-visible + .fv-17__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (forced-colors: active) {
.fv-17__theme {
border-color: CanvasText;
background: Canvas;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
background: Highlight;
color: HighlightText;
}
}
.fv-17:has(#fv-17-theme-dark:checked) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__btn {
color: #100f0d;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
```Here's a working CSS Form Validation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Newsletter Email Pattern Validation
Source: https://codefronts.com/components/css-form-validation/newsletter-email-pattern-validation/
A single email field inline with its button, and an honest account of what type="email" does and does not check. The native type accepts a@b — no dot required — so a pattern tightens it to something with a real domain. The demo also argues the other side: over-strict email regexes reject plus-addressing, long new TLDs and unicode local parts, and those rejections cost you real subscribers.
## HTML
```html
<section class="fv-17" aria-labelledby="fv-17-h">
<fieldset class="fv-17__theme">
<legend class="fv-17__themelegend">Theme</legend>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-auto" checked>
<label class="fv-17__themeopt" for="fv-17-theme-auto">Auto</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-light">
<label class="fv-17__themeopt" for="fv-17-theme-light">Light</label>
<input class="fv-17__themeinput" type="radio" name="fv-17-theme" id="fv-17-theme-dark">
<label class="fv-17__themeopt" for="fv-17-theme-dark">Dark</label>
</fieldset>
<div class="fv-17__stage">
<div class="fv-17__col">
<p class="fv-17__kicker">The Vale Dispatch · Thursdays</p>
<h2 class="fv-17__h" id="fv-17-h">One field, two arguments about it</h2>
<p class="fv-17__lede">The native email type will happily accept <code>alex@b</code>. A pattern fixes that — and a stricter one starts throwing away real subscribers.</p>
<form class="fv-17__form" aria-labelledby="fv-17-h">
<label class="fv-17__label" for="fv-17-email">Email address</label>
<div class="fv-17__unit">
<input class="fv-17__input" id="fv-17-email" name="fv-17-email" type="email" required autocomplete="email" pattern="[^@\s]+@[^@\s]+\.[a-zA-Z][a-zA-Z]+" placeholder="alex@vale.co" value="alex@vale" title="Include a domain with a dot, like vale.co" aria-describedby="fv-17-msg">
<button class="fv-17__btn" type="button">Subscribe</button>
</div>
<p class="fv-17__msg" id="fv-17-msg">
<svg class="fv-17__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="1.7"></circle><path d="M10 5.9v5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"></path><circle cx="10" cy="14.2" r="1.05" fill="currentColor"></circle></svg>
<span>Include a domain with a dot, like alex@vale.co — the address as typed has no top-level domain</span>
</p>
<p class="fv-17__ok">Thanks. Confirm the address from the email we just sent.</p>
</form>
<div class="fv-17__table">
<div class="fv-17__trow fv-17__trow--head"><span>Address</span><span>This pattern</span><span>An over-strict one</span></div>
<div class="fv-17__trow"><span><code>priya+dispatch@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — plus sign</span></div>
<div class="fv-17__trow"><span><code>jordan@studio.design</code></span><span data-v="pass">Accepted</span><span data-v="fail">Rejected — long TLD</span></div>
<div class="fv-17__trow"><span><code>sam@vale</code></span><span data-v="fail">Rejected — no dot</span><span data-v="fail">Rejected</span></div>
<div class="fv-17__trow"><span><code>alex@vale.co</code></span><span data-v="pass">Accepted</span><span data-v="pass">Accepted</span></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
.fv-17 {
--page: #faf8f4;
--ink: #131210;
--muted: #5d564d;
--rule: #ddd7cc;
--accent: #1a1917;
--error: #a03118;
--error-ink: #872710;
--ok: #2f6140;
--display-size: clamp(1.9rem, 6vw, 3.4rem);
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--page);
color: var(--ink);
font-family: system-ui, sans-serif;
}
@supports (color: oklch(0 0 0)) {
.fv-17 {
--error: oklch(0.46 0.16 35);
--error-ink: oklch(0.39 0.16 35);
--ok: oklch(0.46 0.1 155);
}
}
.fv-17 *,
.fv-17 *::before,
.fv-17 *::after {
box-sizing: border-box;
}
.fv-17__stage {
display: grid;
place-items: center;
min-block-size: 100vh;
max-inline-size: 100%;
padding: clamp(1.75rem, 6vw, 5rem) clamp(1rem, 5vw, 3rem);
}
.fv-17__col {
inline-size: min(100%, 44rem);
min-inline-size: 0;
}
.fv-17__kicker {
margin: 0 0 1rem;
padding-block-end: 1rem;
border-block-end: 1px solid var(--rule);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__h {
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-size: var(--display-size);
font-weight: 400;
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
}
.fv-17__lede {
margin: 0 0 2rem;
max-inline-size: 34rem;
font-size: 1.0625rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.fv-17__lede code,
.fv-17__trow code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.86em;
color: var(--ink);
}
.fv-17__form {
display: grid;
gap: 0.5rem;
min-inline-size: 0;
max-inline-size: 34rem;
}
.fv-17__label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__unit {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: stretch;
min-inline-size: 0;
border: 1px solid var(--ink);
background: transparent;
}
.fv-17__input {
min-inline-size: 0;
inline-size: 100%;
block-size: 3.25rem;
padding: 0 1rem;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: transparent;
border: 0;
}
.fv-17__input::placeholder {
color: #a29a8d;
}
.fv-17__input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.fv-17__btn {
min-block-size: 3.25rem;
min-inline-size: 44px;
padding: 0 1.5rem;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--page);
background: var(--accent);
border: 0;
cursor: pointer;
}
.fv-17__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.fv-17__msg,
.fv-17__ok {
display: none;
gap: 0.4375rem;
align-items: flex-start;
margin: 0;
min-inline-size: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.fv-17__msg {
color: var(--error-ink);
}
.fv-17__ok {
color: var(--ok);
font-weight: 600;
}
.fv-17__msg span {
min-inline-size: 0;
}
.fv-17__icon {
flex: none;
inline-size: 1.0625rem;
block-size: 1.0625rem;
margin-block-start: 0.12rem;
}
.fv-17__unit:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) {
border-color: var(--error);
}
.fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:invalid:not(:placeholder-shown):not(:focus)) .fv-17__msg {
display: flex;
}
.fv-17__unit:has(.fv-17__input:user-invalid) {
border-color: var(--error);
}
.fv-17__input:user-invalid ~ .fv-17__btn {
background: var(--error);
}
.fv-17__form:has(.fv-17__input:user-invalid) .fv-17__msg {
display: flex;
}
.fv-17__form:has(.fv-17__input:user-valid) .fv-17__ok {
display: flex;
}
.fv-17__table {
display: grid;
gap: 0;
margin-block-start: 2.5rem;
min-inline-size: 0;
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem 1rem;
padding-block: 0.75rem;
border-block-end: 1px solid var(--rule);
font-size: 0.8125rem;
line-height: 1.45;
min-inline-size: 0;
}
.fv-17__trow span {
min-inline-size: 0;
}
.fv-17__trow--head {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.fv-17__trow [data-v="pass"] {
color: var(--ok);
font-weight: 600;
}
.fv-17__trow [data-v="fail"] {
color: var(--error-ink);
font-weight: 600;
}
@media (max-width: 30rem) {
.fv-17__unit {
grid-template-columns: minmax(0, 1fr);
}
.fv-17__btn {
border-block-start: 1px solid var(--ink);
}
.fv-17__trow {
grid-template-columns: minmax(0, 1fr);
gap: 0.25rem;
}
.fv-17__trow--head {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
[data-theme="dark"] .fv-17:not(:has(#fv-17-theme-light:checked)) .fv-17__btn {
color: #100f0d;
}
.fv-17 {
position: relative;
}
.fv-17__stage {
padding-block-start: 4.75rem;
}
.fv-17__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 6;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 7%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.fv-17__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.fv-17__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 1.875rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.fv-17__themeopt:hover {
opacity: 0.85;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.fv-17__themeinput:focus-visible + .fv-17__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (forced-colors: active) {
.fv-17__theme {
border-color: CanvasText;
background: Canvas;
}
.fv-17__themeinput:checked + .fv-17__themeopt {
background: Highlight;
color: HighlightText;
}
}
.fv-17:has(#fv-17-theme-dark:checked) {
--page: #100f0d;
--ink: #f4f1ea;
--muted: #a09789;
--rule: #2e2a24;
--accent: #f4f1ea;
--error: #ff8256;
--error-ink: #ffa07d;
--ok: #6fc38b;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input::placeholder {
color: #6b6459;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__btn {
color: #100f0d;
}
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:invalid:not(:placeholder-shown):not(:focus) ~ .fv-17__btn,
.fv-17:has(#fv-17-theme-dark:checked) .fv-17__input:user-invalid ~ .fv-17__btn {
color: #1a0d08;
}
```How this works
type="email" is deliberately permissive. The HTML spec's definition of a valid email address is looser than any real mail provider's: alex@localhost and alex@b both pass, because a dotless domain is legal on an intranet. That is the right default for the platform and the wrong default for a newsletter signup, which is why a pattern is worth adding.
The pattern's job is to require a dotted domain, nothing more. [^@\s]+@[^@\s]+\.[a-zA-Z][a-zA-Z]+ asks for a local part, an at sign, a domain, a dot and at least two letters. It does not try to enumerate valid TLDs, does not ban +, and does not cap length — every one of those additions rejects addresses that work. The title attribute explains the rule, because the browser shows it in the bubble.
Over-strict validation is a silent revenue bug. The classic offenders: banning + aliases that people use precisely to track newsletter signups, hardcoding a three-letter TLD limit that rejects .design and .travel, and rejecting unicode local parts that are perfectly deliverable. None of these produce an error report — the user just leaves.
The trap: the only real check is a delivery. No regex knows whether an address exists. Validate the shape, send a confirmation email, and treat the click as the verification. A pattern's whole purpose is to catch the missing dot and the fat-fingered .con before the user waits for a mail that will never arrive.
Make it yours
- Loosen the pattern by dropping the second letter class from the TLD if your audience uses internal domains.
- Change the
titletext — it is what the native bubble shows when the pattern fails. - Retint the accent and error hues via
--accentand--error. - Break the field and button onto separate rows at any width by changing the one
@mediavalue. - Remove the accepted / rejected table for a bare signup unit.
- Set
--display-sizeto scale the serif headline independently of the form.
Gotchas — read before shipping
type="email"alone acceptsa@bwith no dot, so it will not catch a truncated domain.- Banning
+in the local part rejects the alias people most often use for newsletter signups. - Capping the TLD at three characters rejects
.design,.traveland every other long TLD. - No pattern can confirm an address exists — only a delivered confirmation email can, so keep the double opt-in.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 119+ | 16.5+ | 121+ | 119+ |
:user-invalid sets the Chrome 119 / Safari 16.5 floor and is what stops the field flagging an empty box on load; oklch() sets Firefox 113. type="email", pattern and title are Baseline everywhere. On older engines the :invalid:not(:placeholder-shown) rule declared first paints the same state once a value is present.