Skip to content
PПромтбук
RUEN
02Адаптив

Density modes: переключатель comfortable / cozy / compact

Как у Linear и Notion: переключатель плотности интерфейса. Token strategy, что НЕ масштабировать, сохранение preference.

Действуй как principal design-systems инженер. Спроектируй density-режимы для всего продукта по образцу Linear и Notion. Default: {{default_mode}}. Preference хранится по ключу {{storage_key}}.

1. Зачем три режима

  • comfortable — дефолт для новых пользователей, презентации, дашборды для нерегулярных юзеров. Дыхание, читаемость, иерархия. ~16-20% больше воздуха.
  • cozy — золотая середина. Большинство SaaS-продуктов дефолтят сюда после первой недели. Производительность + комфорт.
  • compact — для power users. Linear-режим. Видно больше данных на экран, оператор работает быстрее, но новичка пугает.

Принцип: density меняет только то, что относится к воздуху. Размеры самих сущностей (иконки, аватары, размер шрифта основного текста) — не трогаем. Меняем gap, padding, vertical rhythm, row heights в таблицах.

2. Token strategy: space scale × density factor

База — space scale в CSS-переменных. Density factor умножает spacing-токены, не задевая всё остальное.

:root {
  /* Base space scale (cozy = 1.0) */
  --space-base-1: 4px;
  --space-base-2: 8px;
  --space-base-3: 12px;
  --space-base-4: 16px;
  --space-base-6: 24px;
  --space-base-8: 32px;

  /* Density factor */
  --density-factor: 1;

  /* Public spacing tokens */
  --space-1: calc(var(--space-base-1) * var(--density-factor));
  --space-2: calc(var(--space-base-2) * var(--density-factor));
  --space-3: calc(var(--space-base-3) * var(--density-factor));
  --space-4: calc(var(--space-base-4) * var(--density-factor));
  --space-6: calc(var(--space-base-6) * var(--density-factor));
  --space-8: calc(var(--space-base-8) * var(--density-factor));

  /* Row heights в таблицах */
  --row-height-base: 36px;
  --row-height: calc(var(--row-height-base) * var(--density-factor));
}

[data-density="comfortable"] { --density-factor: 1.2; }
[data-density="cozy"]        { --density-factor: 1; }
[data-density="compact"]     { --density-factor: 0.78; }

Применяется одним атрибутом на <html>:

<html data-density="compact">

3. Что НЕ масштабировать

Категорически нельзя умножать на density factor:

  • Touch targets — минимум 44×44 (iOS) / 48×48 (Android), даже в compact. Иначе сломан тач.
  • Focus rings — толщина и offset фикс. Если в compact кольцо становится 1.5px вместо 2px, accessibility ломается.
  • Иконки — 16/20/24px дискретно, по семантической роли, а не по плотности. Иконка inline-текста — 16px всегда.
  • Шрифты body / основного контента — читаемость не должна зависеть от density. Меняем только UI chrome (метки в таблице, helper text).
  • Border-radius — карточка остаётся карточкой. Радиус не пропорционален плотности.
  • Border width — 1px в cozy = 1px в compact. Иначе hairlines исчезают.
  • Skeleton / shimmer animations — длительность фикс.

Хорошее правило: если элемент несёт смысл сам по себе (иконка, кнопка, текст) — фикс. Если это пустое пространство между элементами — масштабируем.

4. Per-component overrides

Не каждый компонент масштабируется одинаково. Дай управление через атрибут:

/* Кнопка: padding меняется, но не радикально — кнопка не должна стать невидимо тонкой */
.btn {
  padding-block: calc(var(--space-base-2) * clamp(0.85, var(--density-factor), 1.15));
  padding-inline: calc(var(--space-base-4) * clamp(0.85, var(--density-factor), 1.15));
}

/* Таблица: row-height чувствительнее, можно жать сильнее */
.table-row {
  block-size: calc(var(--row-height-base) * var(--density-factor));
}

/* Sidebar item: специальный compact-режим */
[data-density="compact"] .sidebar-item { padding-block: 4px; gap: 6px; }

5. Сохранение preference

Стек: cookie (SSR) + localStorage (быстрый клиентский read) + system event.

// 1. На SSR читаем cookie, проставляем data-density до hydration → no flash
// 2. Клиент: при изменении пишем в localStorage И в cookie
// 3. Reactивно меняем атрибут на <html>

const setDensity = (mode: 'comfortable' | 'cozy' | 'compact') => {
  document.documentElement.dataset.density = mode;
  localStorage.setItem('{{storage_key}}', mode);
  document.cookie = '{{storage_key}}=' + mode + '; path=/; max-age=31536000; SameSite=Lax';
};

// На загрузке
const saved = localStorage.getItem('{{storage_key}}') ?? '{{default_mode}}';
document.documentElement.dataset.density = saved;

Anti-flash: в SSR-фреймворке (Next.js, Astro) прочитай cookie на сервере и поставь data-density прямо в HTML — иначе на первый кадр пользователь увидит дефолт, потом дёрг.

Cross-tab sync:

window.addEventListener('storage', (e) => {
  if (e.key === '{{storage_key}}' && e.newValue) {
    document.documentElement.dataset.density = e.newValue;
  }
});

6. UI переключателя

Не закапывай в settings. Linear показывает в command palette (Cmd+K → "density"), Notion — в appearance settings. Лучше — в settings menu рядом с темой.

Три варианта visualization:

  • Сегментированный контрол (3 кнопки)
  • Slider (если planируешь больше уровней потом)
  • Preview cards — мини-mockup интерфейса в каждом режиме

7. Формат вывода

## Density system

### Tokens
| Token | comfortable | cozy | compact |
|---|---|---|---|
| --space-4 | 19.2px | 16px | 12.5px |
| --row-height | 43px | 36px | 28px |

### Не масштабируем
- Touch targets (≥ 44×44)
- Focus rings (2px / 2px offset)
- Icons (16/20/24 дискретно)
- Body font-size

### Persistence
- Cookie + localStorage + storage event
- SSR reads cookie → no flash

### UI
- Command palette + settings panel
- 3 preview cards

Anti-patterns

  • ❌ Density factor умножает font-size body — текст в compact = 12.5px, читаемость провалена.
  • ❌ Touch targets масштабируются вместе со spacing — в compact кнопки 32×32, мобильные пользователи мажут пальцами.
  • ❌ Focus ring становится 1.2px в compact — accessibility-аудит проваливается мгновенно.
  • ❌ Только localStorage без cookie — SSR-рендер всегда выдаёт default, при загрузке дёрг.
  • ❌ Density через перезагрузку страницы — UX 2010. Меняй мгновенно через CSS-переменную.
  • ❌ Два отдельных набора classes (btn-cozy, btn-compact) — bundle×3, mental load, рассинхрон.
  • ❌ Border-width умножается на factor — в compact hairlines становятся 0.78px, на retina выглядят дрожащими.
  • ❌ Без cross-tab sync — пользователь меняет в одной вкладке, в другой по-старому, фрустрирует.
  • ❌ Сохранение в URL (?density=compact) — расшаривание ссылок ломает плотность у получателя.
  • ❌ Default = compact для нового юзера — отпугивает 80% аудитории за первые 30 секунд.
К подразделу «Адаптив»
Похожие промты