“Give us at least one way to contact you” is a common rule that forms usually implement badly: both the phone and email fields are marked with a required asterisk (so users fill in both), or neither is, and the error appears on whichever field the developer picked, reading “Phone is required” to someone who deliberately chose email.

“At least one of” is a group rule: no single member is required, but the group is. This page, within cross-field dependency logic, implements it for text fields and checkbox groups, with markup that tells users about the requirement before they make a mistake and an error that names the group rather than one field.


Context and prerequisites

Where “at least one” appears:

  • Alternative contact methods — phone or email or postal address.
  • Checkbox groups — “choose at least one topic”, “select the days you are available”.
  • Alternative identifiers — passport number or national ID number.
  • Optional pairs with a floor — “at least one reference” when references are free text fields.

The design points:

  • No member is individually required. Marking both as required changes the rule to “all of”, and assistive technology will announce each as required.
  • The requirement is stated once, on the group. In the legend or a hint: “Provide at least one way for us to contact you.”
  • The error is about the group. “Enter a phone number or an email address”, shown on the group and linked from each member.
  • Each member still validates its own format when filled. An email that is filled in must be a valid email, even though email itself is optional.
Inputs and outcomes for "phone or email" With both empty the group error enter a phone number or an email address appears after submit. With a valid phone and empty email the form is valid. With empty phone and a valid email it is valid. With both valid it is valid. With empty phone and an invalid email the group rule is satisfied in principle but the email's own format error appears, enter an email like name at example dot com, and the group error is not shown. Phone Email Result (empty) (empty) Enter a phone number or an email address. 07700 900123 (empty) valid (empty) [email protected] valid 07700 900123 [email protected] valid (empty) ada@ email: Enter an email like [email protected]. The group error only concerns emptiness. A filled but malformed member gets its own field error instead.

The core pattern: a group rule plus per-member format rules

type Values = Record<string, string | string[] | undefined>;

const filled = (v: string | string[] | undefined) =>
  Array.isArray(v) ? v.length > 0 : typeof v === "string" && v.trim() !== "";

export interface AtLeastOne {
  group: string;                 // id of the group, used for the error slot
  members: string[];             // field names
  message: string;               // names the alternatives
}

export function checkAtLeastOne(values: Values, rule: AtLeastOne) {
  return rule.members.some((m) => filled(values[m])) ? null : { group: rule.group, message: rule.message };
}

// Member format rules run ONLY when the member is filled.
const formatRules: Record<string, (v: string) => string | null> = {
  phone: (v) => (/^[+\d][\d\s()-]{6,}$/.test(v.trim()) ? null : "Enter a phone number, like 07700 900123."),
  email: (v) => (/^[^\s@]+@[^\s@]+\.[^\s@.]+$/.test(v.trim()) ? null : "Enter an email like [email protected]."),
};

export function validateContact(values: Values) {
  const fieldErrors: Record<string, string> = {};
  for (const [name, rule] of Object.entries(formatRules)) {
    const v = values[name];
    if (typeof v === "string" && filled(v)) {
      const e = rule(v);
      if (e) fieldErrors[name] = e;
    }
  }
  // Only report the group rule if no member has its own error: a malformed
  // email already tells the user what to fix.
  const group = Object.keys(fieldErrors).length ? null : checkAtLeastOne(values, {
    group: "contact", members: ["phone", "email"],
    message: "Enter a phone number or an email address.",
  });
  return { fieldErrors, groupError: group };
}
<fieldset aria-describedby="contact-hint contact-error">
  <legend>How can we contact you?</legend>
  <p id="contact-hint">Provide at least one. We'll only use it about your order.</p>
  <p id="contact-error" class="group-error" hidden></p>
  <label for="phone">Phone number (optional)</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel">
  <label for="email">Email address (optional)</label>
  <input id="email" name="email" type="email" autocomplete="email">
</fieldset>

Step-by-step walkthrough

  1. State the requirement in the group, up front. The legend asks the question and a hint says “Provide at least one”, linked to the fieldset with aria-describedby so screen readers hear it on entering the group.
  2. Do not mark members required. Label them as optional, or leave them unmarked, depending on your convention — see indicating required and optional fields.
  3. Validate each member’s format only when filled. Optional does not mean unchecked; a filled field must be valid.
  4. Evaluate the group rule over all members. It passes if any member is filled — for checkbox groups, if any box is checked.
  5. Show the group error on the group. Unhide the error paragraph inside the fieldset and list it in the error summary, linking to the first member.
  6. Clear it when any member is filled. Re-run the rule on every member’s change, using the dependency approach from revalidating dependent fields when a source changes.

Why “optional” labels matter here

With no required markers on either field, users need another signal that the group as a whole is not optional. The legend and hint provide it, but labelling each member “(optional)” strengthens it further: it tells users they do not have to fill in both, which is the most common misreading of these groups. Forms that mark all optional fields as “optional” (rather than marking required ones with an asterisk) handle this case naturally, because the group-level instruction is the only statement of requirement on the page and it stands out.

What a screen-reader user hears On tabbing into the phone field, the user hears the group legend How can we contact you, the hint provide at least one, and the phone label marked optional. They skip both fields and submit; focus moves to the error summary, which says enter a phone number or an email address, linking to the phone field. Following the link, they hear the group again with the error included in its description. After typing an email, the error is removed on the next change. Tab into the group Legend + hint, then "Phone number (optional)". The requirement is heard before any mistake is made. Submit with both empty Summary: "Enter a phone number or an email address." One message about the group, not "Phone is required". Follow the summary link Focus to phone; group error in its description. Either field satisfies it. Type an email Group rule passes; error removed. Email's own format rule now applies.

Failure modes and edge cases

1. Both fields marked required

required on both inputs makes native validation (and screen readers) demand both. The group rule belongs in code; the members stay optional in markup.

2. Error attached to one member

“Phone is required” on the phone field tells an email-preferring user they must give a phone number. The message must name the alternatives and live on the group.

3. Group error plus member error

If the email is malformed and the phone empty, showing both “Enter a phone number or an email address” and “Enter a valid email” is confusing. Suppress the group error when a member has its own error, as the code above does.

4. Checkbox groups

For “choose at least one”, the members are checkboxes sharing a name; the rule checks the array’s length. Put the error on the fieldset, per accessible errors for radio and checkbox groups.

5. Schemas

In Zod, express the rule with superRefine on the object, adding the issue at a group path such as ["contact"] and mapping it to the group slot. A z.union of “has phone” and “has email” objects technically works but produces unhelpful union errors.

Which error to show for the contact group If any filled member has a format problem, show that member's own error and suppress the group error. If at least one member is filled and valid, show no error. Otherwise, when all members are empty, show the group error naming the alternatives, after submit or after the group is touched. A filled member has a format error? Show that member's error yes no At least one member filled? No error yes no Group error "Enter a phone number or an email address."

Verification checklist


Frequently Asked Questions

Should I ask users which contact method they prefer instead?

Often that is better: a radio group “How should we contact you?” with the matching field revealed makes one field conditionally required, which is simpler to understand and validate. Use “at least one of” when you genuinely want any combination.

How should "at least two of" work?

The same way, with a count: filled members must be at least two, and the message states the number (“Choose at least two topics”). Keep the per-member format rules unchanged.

Does aria-required help on the group?

aria-required is not supported on fieldset or group roles in a way screen readers reliably announce. State the requirement in the legend or hint text instead, which every user can see and hear.


Related

← Cross-Field Dependency Logic