Ты разрабатываешь карточку товара: название может быть длинным и обрезается с многоточием. Время чтения статьи — нужно считать слова. Список новостей — заголовки разной длины должны занимать одинаковую высоту. Всё это — типографика в JavaScript: не просто CSS, но и работа со строками, size-вычисления, overflow-логика.
Типографика в интерфейсе влияет на читаемость, UX и доступность. JS-разработчик работает с текстом постоянно: усекает длинные строки, подсчитывает слова, управляет переносами. Неправильные настройки приводят к некрасивым переносам слов, неожиданным размерам и плохой читаемости.
.slice(), .split(), .trim() для работы с текстомscrollWidth > clientWidth для определения обрезанного текста// font-size: используй rem для масштабируемости
// Оптимальный размер основного текста: 1rem (16px)
// line-height: ИСПОЛЬЗУЙ безразмерное значение!
// line-height: 1.5 — 1.5× от font-size (наследуется правильно)
// line-height: 1.5em — фиксируется при наследовании (ПЛОХО!)
// line-height: 24px — не масштабируется (ПЛОХО!)
// Рекомендации:
// Основной текст: 1.5 — 1.8
// Заголовки: 1.1 — 1.3
// Кнопки/labels: 1.0 — 1.2
// UI компоненты: 1.4Чтобы обрезать текст с многоточием, нужны все три свойства:
.truncate {
overflow: hidden; /* обрезаем */
white-space: nowrap; /* запрещаем перенос */
text-overflow: ellipsis; /* добавляем "..." */
}Из JavaScript:
// Определить обрезан ли текст
function isTextTruncated(element) {
return element.scrollWidth > element.clientWidth
}// normal — схлопывает пробелы, перенос строки
// nowrap — схлопывает пробелы, БЕЗ переноса (одна строка)
// pre — сохраняет всё как есть (как в <pre>)
// pre-wrap — сохраняет, но переносит при необходимости
// pre-line — схлопывает пробелы, сохраняет переносы строк// Числовые значения font-weight:
// 100 Thin | 200 ExtraLight | 300 Light | 400 Regular
// 500 Medium | 600 SemiBold | 700 Bold | 800 ExtraBold | 900 Black
// font-family: всегда указывай fallback!
// 'Inter', 'Helvetica Neue', Arial, sans-serif
// system-ui, -apple-system, sans-serif — системный шрифт без загрузки
// 'JetBrains Mono', Consolas, monospace — для кода// letter-spacing: 0.05em — для UPPERCASE кнопок
// letter-spacing: -0.02em — для крупных заголовков (сжатие)
// word-spacing: 0.1em — увеличить расстояние между словами
// text-transform: uppercase | lowercase | capitalize
// Кнопки: text-transform: uppercase + letter-spacing: 0.05emОшибка 1: text-overflow без white-space: nowrap
/* НЕВЕРНО — текст всё равно переносится */
.title {
overflow: hidden;
text-overflow: ellipsis;
/* Забыли white-space: nowrap! */
}
/* ВЕРНО — все три обязательны */
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}Ошибка 2: line-height с единицами
/* ПЛОХО — фиксируется при наследовании */
.parent { font-size: 16px; line-height: 24px; }
.child { font-size: 24px; }
/* child наследует line-height: 24px, но у него font-size: 24px — слишком тесно */
/* ХОРОШО — безразмерное значение масштабируется */
.parent { font-size: 16px; line-height: 1.5; }
.child { font-size: 24px; }
/* child наследует 1.5 → line-height: 36px (1.5 × 24px) */Ошибка 3: Неправильный подсчёт слов
// ПЛОХО — не работает с множественными пробелами
'hello world'.split(' ').length // 3, а не 2!
// ХОРОШО — \s+ соответствует любым пробелам
'hello world'.trim().split(/\s+/).length // 2
''.trim().split(/\s+/).length // 1 — ОШИБКА! Нужна проверка
// ВЕРНО
function countWords(text) {
const trimmed = text.trim()
return trimmed === '' ? 0 : trimmed.split(/\s+/).length
}Работа с типографикой в JS: truncateText, countWords, readingTime, анализ шрифтовой шкалы
// Типографика в JavaScript
// ===== truncateText =====
// Усекает по символам, старается не разрывать слово
function truncateText(text, maxLength, suffix = '...') {
if (text.length <= maxLength) return text
const truncated = text.slice(0, maxLength - suffix.length)
// Ищем последний пробел (не обрываем посередине слова)
const lastSpace = truncated.lastIndexOf(' ')
const result = lastSpace > maxLength * 0.6
? truncated.slice(0, lastSpace)
: truncated
return result + suffix
}
// Усекает по словам
function truncateWords(text, maxWords, suffix = '...') {
const words = text.trim().split(/\s+/)
if (words.length <= maxWords) return text
return words.slice(0, maxWords).join(' ') + suffix
}
console.log('=== truncateText ===')
const article = 'JavaScript — это высокоуровневый язык программирования для создания веб-приложений'
console.log(truncateText(article, 40))
console.log(truncateText(article, 60))
console.log(truncateText(article, 20, '…'))
console.log(truncateText('Коротко', 50)) // без изменений
console.log('\n=== truncateWords ===')
console.log(truncateWords(article, 5))
console.log(truncateWords(article, 10))
console.log(truncateWords('Три слова здесь', 10)) // без изменений
// ===== Подсчёт слов и символов =====
function countWords(text) {
const trimmed = text.trim()
return trimmed === '' ? 0 : trimmed.split(/\s+/).length
}
function countChars(text, includeSpaces = true) {
return includeSpaces ? text.length : text.replace(/\s/g, '').length
}
function readingTime(text, wpm = 200) {
const words = countWords(text)
const minutes = Math.ceil(words / wpm)
return { words, minutes, label: `${minutes} мин` }
}
console.log('\n=== Подсчёт слов ===')
const samples = [
'Одно',
'Hello World',
' много пробелов вокруг ',
'',
'a b c d e f',
]
for (const s of samples) {
console.log(`"${s.trim()}" → ${countWords(s)} слов, ${countChars(s)} симв, ${countChars(s, false)} без пробелов`)
}
console.log('\n=== Время чтения ===')
const texts = [
{ label: 'Твит', text: 'Привет мир! Это тест.' },
{ label: 'Заметка', text: Array(100).fill('слово').join(' ') },
{ label: 'Статья', text: Array(500).fill('слово').join(' ') },
{ label: 'Книга', text: Array(2000).fill('слово').join(' ') },
]
for (const { label, text } of texts) {
const rt = readingTime(text)
console.log(` ${label.padEnd(10)}: ${String(rt.words).padStart(5)} слов → ${rt.label} чтения`)
}
// ===== text-transform =====
console.log('\n=== text-transform ===')
function textTransform(text, mode) {
switch (mode) {
case 'uppercase': return text.toUpperCase()
case 'lowercase': return text.toLowerCase()
case 'capitalize': return text.replace(/(?:^|\s)\S/g, c => c.toUpperCase())
default: return text
}
}
const phrase = 'hello world from javascript'
console.log('uppercase: ', textTransform(phrase, 'uppercase'))
console.log('lowercase: ', textTransform('HELLO WORLD', 'lowercase'))
console.log('capitalize: ', textTransform(phrase, 'capitalize'))
// ===== Типографическая шкала =====
console.log('\n=== Типографическая шкала (base 16px) ===')
const scale = [
{ name: 'xs', rem: 0.75 },
{ name: 'sm', rem: 0.875 },
{ name: 'base', rem: 1 },
{ name: 'lg', rem: 1.125 },
{ name: 'xl', rem: 1.25 },
{ name: '2xl', rem: 1.5 },
{ name: '3xl', rem: 1.875 },
{ name: '4xl', rem: 2.25 },
]
const ROOT_PX = 16
for (const { name, rem } of scale) {
const px = rem * ROOT_PX
const lh = px >= 24 ? 1.2 : 1.6 // заголовки — 1.2, текст — 1.6
console.log(
` text-${name.padEnd(4)}: ${String(rem).padEnd(6)}rem = ${String(px).padEnd(5)}px line-height: ${lh}`
)
}Ты разрабатываешь карточку товара: название может быть длинным и обрезается с многоточием. Время чтения статьи — нужно считать слова. Список новостей — заголовки разной длины должны занимать одинаковую высоту. Всё это — типографика в JavaScript: не просто CSS, но и работа со строками, size-вычисления, overflow-логика.
Типографика в интерфейсе влияет на читаемость, UX и доступность. JS-разработчик работает с текстом постоянно: усекает длинные строки, подсчитывает слова, управляет переносами. Неправильные настройки приводят к некрасивым переносам слов, неожиданным размерам и плохой читаемости.
.slice(), .split(), .trim() для работы с текстомscrollWidth > clientWidth для определения обрезанного текста// font-size: используй rem для масштабируемости
// Оптимальный размер основного текста: 1rem (16px)
// line-height: ИСПОЛЬЗУЙ безразмерное значение!
// line-height: 1.5 — 1.5× от font-size (наследуется правильно)
// line-height: 1.5em — фиксируется при наследовании (ПЛОХО!)
// line-height: 24px — не масштабируется (ПЛОХО!)
// Рекомендации:
// Основной текст: 1.5 — 1.8
// Заголовки: 1.1 — 1.3
// Кнопки/labels: 1.0 — 1.2
// UI компоненты: 1.4Чтобы обрезать текст с многоточием, нужны все три свойства:
.truncate {
overflow: hidden; /* обрезаем */
white-space: nowrap; /* запрещаем перенос */
text-overflow: ellipsis; /* добавляем "..." */
}Из JavaScript:
// Определить обрезан ли текст
function isTextTruncated(element) {
return element.scrollWidth > element.clientWidth
}// normal — схлопывает пробелы, перенос строки
// nowrap — схлопывает пробелы, БЕЗ переноса (одна строка)
// pre — сохраняет всё как есть (как в <pre>)
// pre-wrap — сохраняет, но переносит при необходимости
// pre-line — схлопывает пробелы, сохраняет переносы строк// Числовые значения font-weight:
// 100 Thin | 200 ExtraLight | 300 Light | 400 Regular
// 500 Medium | 600 SemiBold | 700 Bold | 800 ExtraBold | 900 Black
// font-family: всегда указывай fallback!
// 'Inter', 'Helvetica Neue', Arial, sans-serif
// system-ui, -apple-system, sans-serif — системный шрифт без загрузки
// 'JetBrains Mono', Consolas, monospace — для кода// letter-spacing: 0.05em — для UPPERCASE кнопок
// letter-spacing: -0.02em — для крупных заголовков (сжатие)
// word-spacing: 0.1em — увеличить расстояние между словами
// text-transform: uppercase | lowercase | capitalize
// Кнопки: text-transform: uppercase + letter-spacing: 0.05emОшибка 1: text-overflow без white-space: nowrap
/* НЕВЕРНО — текст всё равно переносится */
.title {
overflow: hidden;
text-overflow: ellipsis;
/* Забыли white-space: nowrap! */
}
/* ВЕРНО — все три обязательны */
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}Ошибка 2: line-height с единицами
/* ПЛОХО — фиксируется при наследовании */
.parent { font-size: 16px; line-height: 24px; }
.child { font-size: 24px; }
/* child наследует line-height: 24px, но у него font-size: 24px — слишком тесно */
/* ХОРОШО — безразмерное значение масштабируется */
.parent { font-size: 16px; line-height: 1.5; }
.child { font-size: 24px; }
/* child наследует 1.5 → line-height: 36px (1.5 × 24px) */Ошибка 3: Неправильный подсчёт слов
// ПЛОХО — не работает с множественными пробелами
'hello world'.split(' ').length // 3, а не 2!
// ХОРОШО — \s+ соответствует любым пробелам
'hello world'.trim().split(/\s+/).length // 2
''.trim().split(/\s+/).length // 1 — ОШИБКА! Нужна проверка
// ВЕРНО
function countWords(text) {
const trimmed = text.trim()
return trimmed === '' ? 0 : trimmed.split(/\s+/).length
}Работа с типографикой в JS: truncateText, countWords, readingTime, анализ шрифтовой шкалы
// Типографика в JavaScript
// ===== truncateText =====
// Усекает по символам, старается не разрывать слово
function truncateText(text, maxLength, suffix = '...') {
if (text.length <= maxLength) return text
const truncated = text.slice(0, maxLength - suffix.length)
// Ищем последний пробел (не обрываем посередине слова)
const lastSpace = truncated.lastIndexOf(' ')
const result = lastSpace > maxLength * 0.6
? truncated.slice(0, lastSpace)
: truncated
return result + suffix
}
// Усекает по словам
function truncateWords(text, maxWords, suffix = '...') {
const words = text.trim().split(/\s+/)
if (words.length <= maxWords) return text
return words.slice(0, maxWords).join(' ') + suffix
}
console.log('=== truncateText ===')
const article = 'JavaScript — это высокоуровневый язык программирования для создания веб-приложений'
console.log(truncateText(article, 40))
console.log(truncateText(article, 60))
console.log(truncateText(article, 20, '…'))
console.log(truncateText('Коротко', 50)) // без изменений
console.log('\n=== truncateWords ===')
console.log(truncateWords(article, 5))
console.log(truncateWords(article, 10))
console.log(truncateWords('Три слова здесь', 10)) // без изменений
// ===== Подсчёт слов и символов =====
function countWords(text) {
const trimmed = text.trim()
return trimmed === '' ? 0 : trimmed.split(/\s+/).length
}
function countChars(text, includeSpaces = true) {
return includeSpaces ? text.length : text.replace(/\s/g, '').length
}
function readingTime(text, wpm = 200) {
const words = countWords(text)
const minutes = Math.ceil(words / wpm)
return { words, minutes, label: `${minutes} мин` }
}
console.log('\n=== Подсчёт слов ===')
const samples = [
'Одно',
'Hello World',
' много пробелов вокруг ',
'',
'a b c d e f',
]
for (const s of samples) {
console.log(`"${s.trim()}" → ${countWords(s)} слов, ${countChars(s)} симв, ${countChars(s, false)} без пробелов`)
}
console.log('\n=== Время чтения ===')
const texts = [
{ label: 'Твит', text: 'Привет мир! Это тест.' },
{ label: 'Заметка', text: Array(100).fill('слово').join(' ') },
{ label: 'Статья', text: Array(500).fill('слово').join(' ') },
{ label: 'Книга', text: Array(2000).fill('слово').join(' ') },
]
for (const { label, text } of texts) {
const rt = readingTime(text)
console.log(` ${label.padEnd(10)}: ${String(rt.words).padStart(5)} слов → ${rt.label} чтения`)
}
// ===== text-transform =====
console.log('\n=== text-transform ===')
function textTransform(text, mode) {
switch (mode) {
case 'uppercase': return text.toUpperCase()
case 'lowercase': return text.toLowerCase()
case 'capitalize': return text.replace(/(?:^|\s)\S/g, c => c.toUpperCase())
default: return text
}
}
const phrase = 'hello world from javascript'
console.log('uppercase: ', textTransform(phrase, 'uppercase'))
console.log('lowercase: ', textTransform('HELLO WORLD', 'lowercase'))
console.log('capitalize: ', textTransform(phrase, 'capitalize'))
// ===== Типографическая шкала =====
console.log('\n=== Типографическая шкала (base 16px) ===')
const scale = [
{ name: 'xs', rem: 0.75 },
{ name: 'sm', rem: 0.875 },
{ name: 'base', rem: 1 },
{ name: 'lg', rem: 1.125 },
{ name: 'xl', rem: 1.25 },
{ name: '2xl', rem: 1.5 },
{ name: '3xl', rem: 1.875 },
{ name: '4xl', rem: 2.25 },
]
const ROOT_PX = 16
for (const { name, rem } of scale) {
const px = rem * ROOT_PX
const lh = px >= 24 ? 1.2 : 1.6 // заголовки — 1.2, текст — 1.6
console.log(
` text-${name.padEnd(4)}: ${String(rem).padEnd(6)}rem = ${String(px).padEnd(5)}px line-height: ${lh}`
)
}Реализуй набор типографических утилит для интерфейса. Реализуй: - `truncateText(text, maxLength, suffix)` — усекает текст до `maxLength` символов и добавляет suffix - `countWords(text)` — подсчитывает слова (корректно обрабатывает множественные пробелы и пустую строку) - `readingTime(text, wpm)` — возвращает `{ words, minutes }` — время чтения при `wpm` слов/мин (по умолчанию 200)
truncateText: text.slice(0, maxLength - suffix.length) + suffix. countWords: trim() === "" ? 0 : trim().split(/\s+/).length. readingTime: words = countWords(text), minutes = Math.ceil(words / wpm)