๐ Stop Prop Drilling `disabled`: How <fieldset> Cleaner React Forms

Handling UI state across complex form hierarchies is one of the most common challenges in frontend engineering. ๐ ๏ธ
Recently, while building a feature with a multi-step form containing over 10 nested child components, I needed to disable all user inputs during an active API submit request (loading state).
The initial temptation was to rely on standard React patterns like prop drilling or reaching for React.Context. However, leverage of native HTML APIs often provides a far cleaner, zero-cost solution. ๐ก
Here is how using the standard HTML <fieldset> tag solved this architectural problem without adding extra runtime complexity. ๐
๐ The Problem: State Drilling & Overhead
Consider a scenario where a parent container controls a submitting or isLoading state:
function RegistrationForm() {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
return (
<form onSubmit={handleSubmit}>
{/* Passing isSubmitting down through every layer */}
<PersonalDetailsForm disabled={isSubmitting} />
<AddressInformationForm disabled={isSubmitting} />
<PaymentDetailsForm disabled={isSubmitting} />
<TermsAndConditionsCheck disabled={isSubmitting} />
<SubmitButton isLoading={isSubmitting} />
</form>
);
}
Why this approach falls short:
Prop Drilling Friction: Passing
disableddown 10+ layers pollutes component interfaces and increases boilerplate. ๐ฆContext Over-engineering: Abstracting simple element attributes into a dedicated
FormContextintroduces unnecessary React runtime tree renders for static behavior. โ๏ธMaintenance Overhead: Adding a new input component requires manually wiring up the
disabledprop every single time. ๐งฑ
โก The Solution: Declarative Encapsulation with <fieldset>
Native HTML includes a powerful attribute inheritance feature via the <fieldset> element. ๐ฏ
According to the HTML standard, setting disabled on a <fieldset> automatically propagates the disabled state to all descendant form controls, including:
<input><button><select><textarea>
Optimized Implementation ๐๏ธ
By wrapping child component trees in a single <fieldset>, all nested interactive elements inherit the disabled state declaratively:
function RegistrationForm() {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
return (
<form onSubmit={handleSubmit}>
<fieldset disabled={isSubmitting} className="form-group-reset">
<PersonalDetailsForm />
<AddressInformationForm />
<PaymentDetailsForm />
<TermsAndConditionsCheck />
<SubmitButton />
</fieldset>
</form>
);
}
No matter how deeply nested <input> tags are within PersonalDetailsForm or AddressInformationForm, browsers natively handle their interactability! โจ
๐จ Normalizing Default Browser Styles
Browsers apply default border and padding rules to <fieldset>. You can easily reset this behavior using modern CSS reset rules:
.form-group-reset {
border: 0;
margin: 0;
padding: 0;
min-width: 0; /* Prevents flexbox/grid container overflow issues */
}
Pro Tip: Setting
min-width: 0is critical when working with<fieldset>inside Flexbox or Grid layouts to prevent unexpected element overflow. ๐ก
๐ Key Takeaways
Leverage the Platform: Modern JavaScript frameworks excel at state management, but native HTML attributes like
<fieldset disabled>handle element behavior natively with zero JS runtime cost. ๐Clean Component APIs: Child components remain decoupled from global form loading states, making them more reusable across different screens. ๐งน
Better Accessibility (a11y): Assistive devices natively recognize disabled fieldsets, properly communicating group state changes to screen readers. โฟ
๐ฌ Discussion
Do you rely on web standards when building complex UI components, or do you reach for state abstractions first? Let's discuss in the comments below! ๐