<select> ignores the placeholder attribute. Make the first option the placeholder instead: <option value="" disabled selected hidden>Choose a plan</option>, and add required to the <select> so the form can't be sent until a real option is picked. Turn it grey with select:invalid { color: #6b7280 }. In React, use defaultValue="" on the select, never selected on the option.Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.
Create free accountThe answer: a disabled, empty first option
The placeholder attribute only works on text-like inputs and <textarea>. Put it on a select and nothing happens. The browser simply shows the first option. So you make the first option look and behave like a placeholder:
<select name="plan" required>
<option value="" disabled selected hidden>Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="team">Team</option>
</select>Here is a full working form you can paste into any page. It posts to splitforms, so you get the submissions by email and in your dashboard without writing a backend:
<form action="https://splitforms.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="plan">Plan</label>
<select id="plan" name="plan" required>
<option value="" disabled selected hidden>Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="team">Team</option>
</select>
<button type="submit">Send</button>
</form>
<style>
select:invalid { color: #6b7280; }
option { color: initial; }
</style>If the user clicks Send without choosing, the browser stops the form and shows its "Please select an item in the list" message. Once they pick a plan, plan=pro (or whichever value they chose) is submitted. Get a free access key to try it.
What each attribute does
Each of the four attributes on the placeholder option has one job. Leave one out and the placeholder breaks in a specific way.
value=""makes the placeholder an empty answer. With required on the<select>, an empty value fails validation, so the form can't be submitted while the placeholder is showing.disabledstops the user picking the placeholder again after they have chosen a real option.selectedmakes the placeholder the option shown when the page loads. Without it, the first enabled option is shown instead.hiddenremoves the placeholder from the open dropdown list, so the list only shows real choices.
hidden on an <option> is not handled the same way in every browser. Some, including Safari on iOS, can still show the hidden option in the native picker. That is why you keep disabled as well: if the placeholder does show, it is greyed out and can't be picked. If you group options with optgroup, keep the placeholder outside every group, as the first child of the <select>.
Make the placeholder grey with :invalid
A real input placeholder is grey, but the closed select shows its placeholder in the normal text color. You can fix that with two lines of CSS:
select:invalid {
color: #6b7280; /* grey while the placeholder is showing */
}
option {
color: initial; /* keep the real options readable in the open list */
}This works because a required select with an empty value matches :invalid. As soon as the user picks a real option, the select becomes valid and the text returns to its normal color. It only works when both parts are there: required on the select and value="" on the placeholder. Without required the select is always valid, and without the empty value the placeholder counts as a real answer.
The option { color: initial } rule matters because in some browsers the open list inherits the select's grey color, which would make every option look disabled.
One side effect: :invalid matches from the moment the page loads, before the user has touched anything. That is fine for a grey color. Don't use the same selector for a red error border, or the form looks broken on first load. Use :user-invalid for error styles, which only matches after the user has interacted. For custom error text, see the custom HTML5 validation message guide.
React and Vue: use the select's value, not selected
React: defaultValue="" on the select
React controls which option is chosen through the <select> itself. Putting selected on an <option> triggers a warning and is ignored. For an uncontrolled form, set defaultValue="":
<select name="plan" defaultValue="" required>
<option value="" disabled hidden>Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>For a controlled select, start the state at an empty string and pass it as value:
function PlanPicker() {
const [plan, setPlan] = useState("");
return (
<select name="plan" value={plan} required
onChange={(e) => setPlan(e.target.value)}>
<option value="" disabled hidden>Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
);
}Don't start the state at undefined or null. React treats that as an uncontrolled select and warns when it switches to controlled after the first pick.
Vue: v-model starting at ''
In Vue, v-model picks the option whose value matches, so start the bound value at an empty string. The empty-value placeholder is then selected on load:
<script setup>
import { ref } from "vue";
const plan = ref("");
</script>
<template>
<select v-model="plan" name="plan" required>
<option value="" disabled hidden>Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
</template>Resetting the form brings the placeholder back
form.reset() (or a reset button) puts every control back to its default. For a select, the default is the option with the selected attribute in the HTML, which is your placeholder. So after a successful submit you can clear the form and the placeholder returns, grey and all:
form.addEventListener("submit", async (e) => {
e.preventDefault();
await fetch(form.action, { method: "POST", body: new FormData(form) });
form.reset(); // the select shows "Choose a plan" again
});In a controlled React form, reset() won't change your state. Set it back yourself with setPlan("").
Common mistakes
- Forgetting
value="". An option without a value submits its text, sorequiredpasses and you receiveplan=Choose a plan. - Using
disabledwithoutselected. The browser skips the disabled option and shows the first real option instead, so the placeholder never appears and users can send the form without making a choice. - Using
placeholder=on the select. It isn't a valid attribute for<select>and does nothing. - Leaving out
required. The empty value is then allowed, the form sendsplan=, and the:invalidgrey style never applies. - Using
selectedon an option in React. React ignores it. UsedefaultValueorvalueon the select.
FAQ
Does the placeholder attribute work on a select?
No. placeholder is only defined for text-like inputs and textarea. A <select> ignores it. Use a first <option value="" disabled selected hidden> with your hint text instead, and add required to the <select>.
How do I make a select placeholder grey?
Add required to the <select> and give the placeholder option value="". While it is selected, the select matches :invalid, so select:invalid { color: #6b7280 } turns it grey. Add option { color: initial } so the real options in the open list keep their normal color.
How do I add a placeholder to a select in React?
Put defaultValue="" on the <select> (or value="" with state if it is controlled) and add <option value="" disabled hidden>Choose a plan</option>. Don't put selected on the option: React warns about it and ignores it.
Why does my select placeholder text get submitted?
The placeholder option has no value attribute, so the browser submits its text instead. Add value="" to the placeholder option. With required on the <select>, the empty value then blocks the form until the user picks a real option.
Does hidden on an option work on iPhone?
Not reliably. Some browsers, including Safari on iOS, can still show a hidden option in the picker. That is why the placeholder should also be disabled: even if it shows, it can't be picked again.
References: MDN: the select element and MDN: :invalid.