Поле ввода с подсказкой внутри — одна из самых частых деталей формы и одновременно одна из самых неудобных. В макете подсказка нарисована приглушённым серым, а в браузере она то слишком светлая, чтобы её прочитать, то внезапно берёт цвет введённого текста. Причина простая: собственных стилей у подсказки нет — её рисует сам браузер, и каждый движок рисует по-своему.

Управляет этим один псевдоэлемент. Разберём, как он устроен, какие свойства внутри него действительно применяются, как поменять цвет и не испортить читаемость формы, и что делать с вендорными селекторами, которые до сих пор кочуют по чужим стилям.

Почему подсказка в каждом браузере выглядит по-своему

Атрибут placeholder у поля — это просто строка. Как её показать, решает браузер, и решения у браузеров разные. Chrome рисует подсказку своим серым (darkgray) и на цвет самого поля не смотрит. Firefox поступает иначе: берёт цвет введённого текста и приглушает его до 54 % непрозрачности.

.field {
  color: #1a4d2e;   /* цвет введённого текста */
}
/* подсказку никто не трогал:
   Firefox нарисует её приглушённым зелёным,
   Chrome — своим серым, цвет поля он проигнорирует */

На светлом фоне и с тёмным текстом разница почти не заметна. Стоит взять нестандартный цвет текста или посадить поле на цветную подложку — и форма в двух браузерах выглядит откровенно по-разному. Поэтому цвет подсказки почти всегда задают явно, а не полагаются на дефолт.

Поля с тремя разными цветами текста в Chrome и Firefox: Firefox приглушает цвет поля, Chrome рисует подсказку серым

Псевдоэлемент ::placeholder: синтаксис и куда его вешать

Подсказка адресуется псевдоэлементом ::placeholder. Правило без селектора перед ним попадёт во все поля на странице сразу:

::placeholder {
  color: #7a4ec6;
}

На практике так делать не рекомендуется: под глобальное правило попадут и поиск в шапке, и форма подписки, и поле комментария, у которых оформление обычно разное. Полезнее привязываться к конкретному полю или к классу:

.search-field::placeholder {
  color: #7a4ec6;
  font-style: italic;
}

Псевдоэлемент работает не только у <input>, но и у <textarea> — отдельного селектора для многострочного поля заводить не нужно.

Главный приём предсказуемости — задать color явно. Когда цвет указан, Firefox перестаёт приглушать подсказку и показывает ровно то, что написано. Если нужно, чтобы подсказка совпадала по цвету с введённым текстом, есть ключевое слово currentColor:

.field::placeholder {
  color: currentColor;   /* тот же цвет, что у текста в поле */
}

В чужих стилях рядом с цветом часто стоит opacity: 1. Это наследство: в старых сборках Firefox приглушение было отдельным объявлением прозрачности в стилях браузера, и без сброса цвет действительно не докрашивался. Современному Firefox достаточно явного color. Лишняя строка ничего не сломает, но и смысла в ней уже нет.

Что внутри ::placeholder действительно работает

Частая история: разработчик пишет внутри псевдоэлемента отступ или рамку, ничего не происходит, и дальше идут поиски опечатки. Опечатки там нет. Спецификация CSS Pseudo-Elements разрешает внутри ::placeholder только то подмножество свойств, которое применимо к псевдоэлементу ::first-line — то есть к оформлению строки текста, а не контейнера.

Группа свойств Применяется Игнорируется
Цвет и прозрачность color, opacity
Шрифт font-family, font-size, font-weight, font-style, font-variant
Фон background-color, background-image и остальные background-*
Набор текста letter-spacing, word-spacing, line-height, text-transform, text-decoration, text-shadow, vertical-align text-align (задаётся на самом поле)
Контейнер padding, margin, border, width, height
Раскладка и эффекты display, position, transform, content

Отсюда практическое правило: всё, что двигает подсказку внутри поля, делается на самом поле, а не на псевдоэлементе. Отступ слева — это padding поля, и он одновременно сдвинет и введённый текст, что как раз и нужно.

/* так ничего не произойдёт */
.field::placeholder {
  padding-inline-start: 12px;
}

/* так правильно */
.field {
  padding-inline-start: 12px;
}

Контраст: ради чего подсказку вообще трогают

Дефолтный светло-серый почти никогда не проходит по контрасту. Требование WCAG (Web Content Accessibility Guidelines — международные рекомендации по доступности веб-контента) — коэффициент контрастности не ниже 4,5:1 для обычного текста и не ниже 3:1 для крупного. Крупным считается текст от 18,66 px жирного начертания или от 24 px обычного. Серая подсказка на белом фоне обычно даёт что-то в районе 2,5:1 — на солнце или на дешёвой матрице её не видно вовсе.

Но и обратная крайность работает против пользователя. Если подсказка по контрасту совпадает с введённым текстом, человек принимает её за уже заполненное значение: пропускает поле, отправляет форму и получает ошибку валидации. Поэтому цель — не «сделать потемнее», а попасть в узкий коридор: читаемо, но заметно отличается от реального ввода.

.field {
  background: #ffffff;
  color: #1b1f27;            /* введённый текст, контраст ~16,5:1 */
}

.field::placeholder {
  color: #5b6472;            /* подсказка, контраст ~6:1 */
}

Разницу удобнее держать не только по яркости, но и по начертанию: курсив или чуть меньший размер отделяют подсказку от значения надёжнее, чем десять процентов серого. Как подбирать пару «текст — фон» по числам, разбирали в статье про выбор цвета шрифта по цвету фона.

Отдельно стоит помнить про режим высокой контрастности Windows (в CSS ему соответствует медиа-запрос forced-colors): система подменяет цвета своими, и подсказка начинает выглядеть ровно как введённый текст. Никакими стилями это не обходится — поэтому подсказка не должна быть единственным носителем смысла. Подпись поля живёт в <label>, а подсказка внутри поля — только дополнение к ней; подробный разбор аргументов — в заметке placeholder — это не label.

Светлая и тёмная тема одним правилом

Цвет подсказки — классический кандидат на дублирование в тёмной теме: то, что читается на белом, на тёмном фоне превращается в грязь. Раньше выход был один — продублировать правило в медиа-запросе:

.field::placeholder {
  color: #5b6472;
}

@media (prefers-color-scheme: dark) {
  .field::placeholder {
    color: #9aa4b2;
  }
}

Сейчас то же самое пишется одной строкой через функцию light-dark(). Она принимает два значения и сама выбирает нужное по текущей схеме — при условии, что схема объявлена свойством color-scheme:

:root {
  color-scheme: light dark;
}

.field::placeholder {
  color: light-dark(#5b6472, #9aa4b2);
}
Поддержка браузерами
chrome
Chrome
123
firefox
Firefox
120
edge
Edge
123
safari
Safari
17.5
opera
Opera
109

Есть и второй подход, при котором тему вообще не приходится перечислять. Подсказка — это тот же текст поля, только разбавленный его фоном; функция color-mix() умеет смешивать два цвета в заданной пропорции, и результат сам поедет за темой:

.field {
  --field-bg: light-dark(#ffffff, #12161c);

  background: var(--field-bg);
  color: light-dark(#1b1f27, #e6e9ef);
}

.field::placeholder {
  /* 62 % цвета текста, остальное — фон поля */
  color: color-mix(in oklab, currentColor 62%, var(--field-bg));
}

Смешивать лучше в oklab или oklch, а не в srgb: в этих пространствах середина между двумя цветами выглядит серединой и для глаза, без провалов в грязь. Почему так — разбирали в статье про OKLCH и предсказуемый цвет в CSS. Про сам переключатель тем и про prefers-color-scheme есть отдельный разбор.

Вендорные селекторы: наследство, которое ломает правило

В чужих стилях и в старых ответах на форумах цвет подсказки почти всегда задаётся набором из четырёх-пяти селекторов. Вот что это было:

Селектор Кому он был нужен
::-webkit-input-placeholder Chrome и Opera до перехода на стандартный псевдоэлемент, Safari до 10.1
:-moz-placeholder Firefox 4–18, ещё в виде псевдокласса с одним двоеточием
::-moz-placeholder Firefox 19–50, уже псевдоэлемент, но всё ещё с префиксом
:-ms-input-placeholder Internet Explorer 10–11 и первый Edge на движке EdgeHTML

Сегодня писать их в новом коде незачем: все живые браузеры понимают стандартный ::placeholder, а Internet Explorer снят с поддержки Microsoft в июне 2022 года. Но знать их полезно — они встречаются в легаси, и с ними связана ловушка, на которой правило перестаёт работать целиком.

Список селекторов в CSS работает по принципу «всё или ничего»: если браузер не понимает хотя бы один селектор в перечислении через запятую, он выбрасывает всё правило. А вендорный селектор чужого движка как раз и является для него непонятным.

/* правило не сработает НИ В ОДНОМ браузере:
   каждый споткнётся о чужой префикс и отбросит весь блок */
::placeholder,
::-webkit-input-placeholder,
:-ms-input-placeholder {
  color: #7a4ec6;
}
/* каждому селектору — своё правило */
::placeholder {
  color: #7a4ec6;
}

::-webkit-input-placeholder {
  color: #7a4ec6;
}

Обойти это «прощающими» списками вроде :is() не получится: псевдоэлементы внутрь них помещать нельзя. Так что вариант только один — отдельное правило на каждый селектор.

Псевдокласс :placeholder-shown: когда одного цвета мало

У ::placeholder есть парный псевдокласс — :placeholder-shown. Он вешается не на подсказку, а на само поле и совпадает ровно до тех пор, пока подсказка видна, то есть пока в поле ничего не ввели. Разница важная: псевдоэлемент красит текст подсказки, псевдокласс позволяет реагировать на факт пустого поля — и красить хоть рамку, хоть соседние элементы.

Самый практичный сценарий — показывать кнопку очистки только тогда, когда очищать есть что. Никакого JavaScript для этого не нужно:

.search__clear {
  visibility: hidden;
}

/* поле НЕ показывает подсказку — значит, в нём что-то есть */
.search__input:not(:placeholder-shown) ~ .search__clear {
  visibility: visible;
}

На этом же псевдоклассе строится приём с подписью, которая изображает подсказку в пустом поле и уезжает наверх, когда человек начал печатать. Важный нюанс: поле при этом должно иметь непустой placeholder — обычно туда кладут один пробел, иначе псевдокласс не сработает. И это не отменяет настоящий <label> в разметке: подпись должна оставаться подписью для скринридера, а не превращаться в декорацию. Как собрать такую форму целиком — в разборе HTML для формы логина и регистрации, а сам псевдокласс вместе с остальными состояниями полей разобран в обзоре современных псевдоклассов CSS.

Частые ошибки

  • Правило ::placeholder без селектора перед ним. Под него попадают все поля сайта разом, включая поиск в шапке и форму комментария с другим оформлением.
  • Подгонка цвета по макету без проверки контраста. Дизайнер видит подсказку на калиброванном мониторе в тёмной комнате, пользователь — на телефоне под солнцем.
  • Слишком тёмная подсказка. Её принимают за уже введённое значение и пропускают поле.
  • Перечисление вендорных селекторов через запятую. Правило не сработает нигде — каждому селектору нужен свой блок.
  • Попытка задать padding, border или transform внутри псевдоэлемента. Геометрия настраивается на самом поле.
  • Подсказка вместо подписи. Она исчезает при вводе, обрезается по ширине поля и по-разному читается вспомогательными технологиями.

Итог

Для цвета подсказки достаточно одного псевдоэлемента и одного явного объявления color — всё остальное в старых рецептах либо устарело вместе с браузерами, либо мешает. Внутри ::placeholder работают только «текстовые» свойства: цвет, прозрачность, шрифт, фон и набор; геометрия настраивается на поле.

Реальная работа тут не в синтаксисе, а в подборе цвета. Подсказка должна уверенно проходить по контрасту и при этом заметно отличаться от введённого значения — у этого коридора узкие стенки с обеих сторон. Для тёмной темы дублировать правило больше не нужно: light-dark() выбирает значение по схеме, а color-mix() позволяет вывести цвет подсказки из цвета текста и фона поля, чтобы он ехал за темой сам.