Lista rozwijana w HTML – select, option, optgroup i datalist

Mateusz Bogolubow

13-10-2026

(aktualizacja: 13-10-2026)

Jak zrobić listę rozwijaną w HTML i nie wpaść w typowe pułapki? Pokazuję element select krok po kroku: od pozycji i grup, przez wartość domyślną i odczyt w JavaScript, po stylowanie w CSS.

Lista rozwijana w HTML – select, option, optgroup i datalist
Listę rozwijaną w HTML tworzysz elementem select, w którym każdą pozycję zapisujesz jako option, a wysyłana do serwera wartość trafia do atrybutu value. Pozycje możesz grupować elementem optgroup, a wybór wielu wartości włączysz atrybutem multiple. Jeśli użytkownik ma także móc wpisać własną wartość, zamiast select użyj pola input z elementem datalist.

Lista rozwijana to jeden z najczęściej używanych elementów formularzy: wybierasz w niej kraj, kategorię, rozmiar albo termin. W HTML da się ją zrobić bez ani jednej linijki JavaScriptu, ale kilka szczegółów – jak pusta pozycja startowa, wartość wysyłana do serwera czy ograniczone możliwości stylowania – potrafi zaskoczyć na początku. W tym poradniku przejdziemy przez nie krok po kroku.

Co to jest lista rozwijana w HTML?

Lista rozwijana (ang. dropdown list albo select) to element formularza, w którym użytkownik wybiera jedną lub kilka wartości z gotowego zestawu. Tworzysz ją elementem select, a pozycje zapisujesz jako elementy option. Przeglądarka sama pokazuje rozwijany panel, obsługuje klawiaturę i na telefonach otwiera natywny wybór wartości, więc nie musisz niczego dopisywać.

Nie myl jej z menu rozwijanym w nawigacji strony. Lista rozwijana służy do wybierania wartości w formularzu, a menu rozwijane – do przechodzenia między podstronami. Jeśli szukasz tego drugiego, zajrzyj do artykułu o tym, jak stworzyć wielopoziomowe rozwijane menu w HTML i CSS.

Jak zrobić listę rozwijaną w HTML?

Całość składa się z elementu select i zagnieżdżonych w nim elementów option. Użytkownik widzi tekst znajdujący się między znacznikami option, natomiast do serwera trafia wartość z atrybutu value. Do listy dodaj też element label z atrybutem for powiązanym z id listy – dzięki temu pole jest zrozumiałe dla czytników ekranowych, a kliknięcie w napis aktywuje listę.

<label for="course">Wybierz kurs</label>
<select id="course" name="course">
  <option value="html-css">HTML i CSS</option>
  <option value="js">JavaScript</option>
  <option value="php">PHP</option>
</select>

Najważniejsze atrybuty zebrałem w tabeli:

AtrybutElementCo robi
nameselectnazwa pola, pod którą wybrana wartość trafia do serwera
valueoptionwartość wysyłana do serwera; może różnić się od widocznego tekstu
selectedoptionzaznacza pozycję domyślnie
disabledselect lub optionwyłącza całą listę albo pojedynczą pozycję
requiredselectwymusza wybór wartości przed wysłaniem formularza
multipleselectpozwala wybrać kilka pozycji naraz
sizeselectokreśla, ile pozycji jest widocznych bez rozwijania

Jak ustawić domyślną wartość listy rozwijanej?

Domyślnie przeglądarka zaznacza pierwszą pozycję. Jeśli chcesz zaznaczyć inną, dodaj jej atrybut selected:

<select id="course" name="course">
  <option value="html-css">HTML i CSS</option>
  <option value="js" selected>JavaScript</option>
  <option value="php">PHP</option>
</select>

Zaznaczona z góry pozycja ma jednak wadę: użytkownik może wysłać formularz, nie dokonując świadomego wyboru. Dlatego warto zrozumieć, jak działa pusta pozycja startowa.

Jak dodać pustą pozycję startową (placeholder) w liście rozwijanej?

Dodaj na początku listy pozycję z pustą wartością i oznacz całe pole atrybutem required:

<select id="course" name="course" required>
  <option value="">Wybierz kurs…</option>
  <option value="html-css">HTML i CSS</option>
  <option value="js">JavaScript</option>
</select>

Przeglądarka nie wyśle takiego formularza, dopóki użytkownik nie wybierze pozycji o niepustej wartości, i sama pokaże komunikat o błędzie. Jeśli chcesz sprawdzać poprawność także własnym kodem, przeczytaj artykuł Walidacja formularza w JavaScript.

Jak pogrupować pozycje listy rozwijanej (optgroup)?

Przy dłuższych listach użytkownikowi łatwiej się odnaleźć, gdy pozycje są pogrupowane. Służy do tego element optgroup z atrybutem label, w którym umieszczasz powiązane ze sobą elementy option:

<select id="topic" name="topic">
  <optgroup label="Front-end">
    <option value="html">HTML</option>
    <option value="css">CSS</option>
  </optgroup>
  <optgroup label="Back-end">
    <option value="php">PHP</option>
    <option value="python">Python</option>
  </optgroup>
</select>

Nazwa grupy jest widoczna na liście, ale sama nie jest pozycją, którą można wybrać.

Jak zrobić listę rozwijaną z wielokrotnym wyborem?

Atrybut multiple zamienia listę rozwijaną w pole, w którym można zaznaczyć kilka pozycji. Wtedy przestaje być ona „rozwijana” – przeglądarka pokazuje kilka pozycji naraz, a ich liczbę możesz ustawić atrybutem size:

<label for="skills">Które technologie znasz?</label>
<select id="skills" name="skills" multiple size="4">
  <option value="html">HTML</option>
  <option value="css">CSS</option>
  <option value="js">JavaScript</option>
  <option value="php">PHP</option>
</select>

Zaznaczanie kilku pozycji wymaga na komputerze przytrzymania klawisza Ctrl (na macOS – Cmd). To bywa niewygodne i mało oczywiste dla użytkowników, więc przy kilku opcjach często lepsze od takiej listy są pola wyboru (checkbox).

Jak odczytać wybraną wartość listy rozwijanej w JavaScript?

Wybraną wartość odczytasz we właściwości value elementu select, a zmianę wyboru złapiesz zdarzeniem change:

const select = document.querySelector('#course');

select.addEventListener('change', () => {
  console.log(select.value); // wartość z atrybutu value, np. "js"
  console.log(select.options[select.selectedIndex].text); // widoczny tekst, np. "JavaScript"
});

select.value = 'php'; // ustawienie wybranej pozycji z poziomu kodu

Przy liście z atrybutem multiple właściwość value zwróci tylko pierwszą zaznaczoną pozycję. Wszystkie wybrane wartości pobierzesz tak:

const skills = document.querySelector('#skills');
const selected = [...skills.selectedOptions].map((option) => option.value);

Jak ostylować listę rozwijaną w CSS?

Zamkniętą listę ostylujesz tak jak inne pola formularza: ustawisz obramowanie, wypełnienie, tło i czcionkę. Ograniczenia zaczynają się przy rozwijanym panelu z pozycjami. W wielu przeglądarkach rysuje go system operacyjny, więc wpływ CSS na wygląd samych elementów option jest mniejszy i bywa różny w zależności od przeglądarki oraz systemu.

select {
  appearance: none;
  padding: 10px 40px 10px 14px;
  border: 1px solid #3d3aeb;
  border-radius: 6px;
  background-color: #fff;
  font: inherit;
}

Właściwość appearance: none usuwa natywny wygląd listy razem ze strzałką, dlatego własną strzałkę dodaj np. jako obraz tła (background-image). Pamiętaj też o stanie :focus – użytkownicy klawiatury muszą widzieć, na której liście aktualnie się znajdują.

Dlaczego lista rozwijana wygląda inaczej na różnych systemach?

Element select jest tak zwanym kontrolką natywną: jego wygląd – zwłaszcza rozwiniętego panelu – zależy od przeglądarki i systemu operacyjnego. Ta sama lista wygląda inaczej w Windows, w macOS i na telefonie z Androidem czy iOS, a na urządzeniach mobilnych zamiast panelu pod polem często otwiera się systemowe okno wyboru. Dla użytkownika to zaleta, bo każdy zna takie zachowanie ze swojego urządzenia, ale dla projektanta oznacza brak pewności, że zobaczysz dokładnie to, co w makiecie.

Z CSS-a zawsze zmienisz wygląd samego, zamkniętego pola: obramowanie, wypełnienie, tło, czcionkę i strzałkę. Nie masz natomiast pełnej kontroli nad rozwijanym panelem i wyglądem pozycji. Nowsze przeglądarki oparte na Chromium oferują wartość appearance: base-select, która pozwala ostylować także panel, ale przed użyciem jej na produkcji sprawdź aktualne wsparcie w dokumentacji MDN.

Jak mieć pełną kontrolę nad wyglądem listy rozwijanej?

Jeśli projekt wymaga identycznego wyglądu na każdym urządzeniu, wzorzec jest zawsze ten sam: własny wygląd robisz osobno, a działanie łączysz z natywnym select za pomocą JavaScriptu. Natywny select zostaje w formularzu jako źródło prawdy – to on przechowuje wybraną wartość, to on jest wysyłany do serwera, a jego brak oznaczałby problemy z walidacją i dostępnością. Obok niego budujesz własny przycisk i listę pozycji, które tylko go „odzwierciedlają”:

<div class="dropdown">
  <select id="course" name="course" class="dropdown__native" tabindex="-1">
    <option value="html-css">HTML i CSS</option>
    <option value="js">JavaScript</option>
    <option value="php">PHP</option>
  </select>

  <button type="button" class="dropdown__button" aria-haspopup="listbox" aria-expanded="false">
    HTML i CSS
  </button>
  <ul class="dropdown__list" role="listbox" hidden>
    <li role="option" data-value="html-css" aria-selected="true">HTML i CSS</li>
    <li role="option" data-value="js" aria-selected="false">JavaScript</li>
    <li role="option" data-value="php" aria-selected="false">PHP</li>
  </ul>
</div>

Natywny select chowamy wizualnie, ale nie przez display: none. Taka kontrolka nie może otrzymać fokusu, więc przeglądarka nie pokaże błędu walidacji dla pola z atrybutem required, a w konsoli zobaczysz komunikat, że nie da się go zogniskować.

.dropdown {
  position: relative;
}

.dropdown__native {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

Teraz połączmy oba elementy. Kliknięcie własnej pozycji ustawia wartość natywnego select, a każda zmiana natywnego select (także ta zrobiona z poziomu kodu) odświeża wygląd własnej listy:

const dropdown = document.querySelector('.dropdown');
const select = dropdown.querySelector('.dropdown__native');
const button = dropdown.querySelector('.dropdown__button');
const list = dropdown.querySelector('.dropdown__list');

function setOpen(open) {
  list.hidden = !open;
  button.setAttribute('aria-expanded', String(open));
}

function render() {
  button.textContent = select.options[select.selectedIndex].text;
  list.querySelectorAll('[role="option"]').forEach((item) => {
    item.setAttribute('aria-selected', String(item.dataset.value === select.value));
  });
}

button.addEventListener('click', () => setOpen(list.hidden));

list.addEventListener('click', (event) => {
  const item = event.target.closest('[role="option"]');
  if (!item) return;

  select.value = item.dataset.value;
  select.dispatchEvent(new Event('change', { bubbles: true }));
  setOpen(false);
});

select.addEventListener('change', render);

Zwróć uwagę na dispatchEvent. Ustawienie select.value z poziomu kodu nie wywołuje zdarzenia change – robisz to ręcznie, żeby reszta aplikacji (i nasz własny render) dowiedziała się o zmianie.

To jednak dopiero szkic. Do gotowego komponentu brakuje m.in. obsługi klawiatury (strzałki, Enter, Escape, wpisywanie pierwszych liter pozycji), zamykania listy po kliknięciu poza nią i zarządzania fokusem. Dlatego zanim zdecydujesz się na takie rozwiązanie, zastanów się, czy wygląd natywnego pola naprawdę jest dla Twojego projektu problemem.

Czym różni się select od datalist?

Obie konstrukcje pokazują listę podpowiedzi, ale działają zupełnie inaczej. Select pozwala wybrać wyłącznie pozycję z listy, a datalist podpowiada wartości w zwykłym polu tekstowym i pozwala wpisać własną:

<label for="city">Miasto</label>
<input id="city" name="city" list="cities">
<datalist id="cities">
  <option value="Kraków"></option>
  <option value="Gdańsk"></option>
  <option value="Wrocław"></option>
</datalist>
selectinput i datalist
Czy użytkownik może wpisać własną wartość?nietak
Czy wysyłana wartość jest zawsze z listy?taknie, trzeba ją zweryfikować
Typowe zastosowaniewybór kraju, kategorii, rozmiaruwyszukiwarka z podpowiedziami, popularne miasta

Skoro datalist nie gwarantuje, że wartość pochodzi z listy, po stronie serwera (i najlepiej także w przeglądarce) sprawdź, czy wpisany tekst jest dozwolony.

Kiedy zamiast select zbudować własną listę rozwijaną?

Najczęściej – wcale. Natywny select jest dostępny dla klawiatury i czytników ekranowych, działa na telefonach i nie wymaga utrzymywania kodu. Własna lista ma sens, gdy projekt wymaga identycznego wyglądu na wszystkich urządzeniach albo gdy pozycje muszą zawierać coś, czego option nie obsłuży, na przykład obrazki czy pole wyszukiwania. Wtedy zastosuj wzorzec opisany wyżej: zostaw natywny select w formularzu i połącz go z własnym wyglądem przez JavaScript. Pamiętaj tylko, że przejmujesz odpowiedzialność za obsługę klawiatury, fokusu i atrybutów ARIA, a to znacznie więcej pracy, niż się wydaje.

Jakie błędy najczęściej popełniają początkujący?

  • Brak atrybutu name. Pole bez nazwy nie zostanie wysłane razem z formularzem – serwer w ogóle go nie zobaczy.
  • Brak atrybutu value. Jeśli go pominiesz, do serwera trafi widoczny tekst pozycji. Gdy później zmienisz opis, zmieni się też wysyłana wartość.
  • Pusta pozycja startowa bez required. Użytkownik może wtedy wysłać formularz bez wybierania czegokolwiek.
  • Brak elementu label. Czytnik ekranowy nie wie, czego dotyczy lista, a kliknięcie w opis nic nie robi.
  • Lista rozwijana jako nawigacja. Nie używaj select do przechodzenia między podstronami. Nawigacja powinna składać się z linków, na przykład w rozwijanym menu.
Konsultacja i warsztaty devmentor.pl

Masz pytania?