최신 CSS 기능 및 트렌드
- Things You Can Do With CSS Today — Smashing Magazine
- The Future of CSS: Cascade Layers (CSS @layer)
- The Undeniable Utility Of CSS :has • Josh W. Comeau
- An Interactive Guide to CSS Container Queries
- 컨테이너 쿼리 사용 방법 | web.dev
- What if you used Container Units for everything?
레이아웃과 반응형 디자인
- Fluid Sizing Instead Of Multiple Media Queries? — Smashing Magazine
- Complex conditional width using flex-basis with clamp
- Using grid to split a table cell
- TablesNG — Improvements to
<table>rendering in Chromium
CSS 애니메이션 및 효과
- A CSS-only, animated, wrapping underline.
- Cubic Bézier: from math to motion
- Zero Trickery Custom Radios and Checkboxes - CSS-Tricks
- CSS One-Liners to Improve (Almost) Every Project
- 6 CSS Snippets Every Front-End Developer Should Know In 2025
텍스트 및 타이포그래피
스타일 가이드 및 접근성
- Naming Variables In CSS
- Margin considered harmful
- Defensive CSS - Ahmad Shadeed
- The wasted potential of CSS attribute selectors
CSS와 JavaScript의 조합
- Constructable Stylesheets: seamless reusable styles
- Replace JavaScript Dialogs With New HTML Dialog | CSS-Tricks
- How to prevent scrolling the page on iOS Safari 15
CSS 아키텍처 및 모듈화
background
인라인 요소에 bold 스타일이 적용될 경우 레이아웃 시프팅 현상이 발생하기 때문에 해당 이슈를 해결하는 방법들.
- html - Inline elements shifting when made bold on hover - Stack Overflow1
- Bold on Hover… Without the Layout Shift | CSS-Tricks2
Footnotes
텍스트 ellipsis는 컨텍스트마다 패턴이 다르다.
언제 — 먼저 어느 컨텍스트인지 가른다.
| 컨텍스트 | 어디로 |
|---|---|
display: table-cell |
아래 절차 |
| flexbox | 맨 아래 참고 링크 |
| 여러 줄(multiline) | 맨 아래 참고 링크 |
table cell
절차
- 표에
table-layout: fixed를 준다. - 셀에
max-width: 0과overflow셋(overflow: hidden·text-overflow: ellipsis·white-space: nowrap)을 준다.
table {
table-layout: fixed;
width: 100%;
}
col:nth-child(1) { width: 30%; }
col:nth-child(2) { width: 40%; }
col:nth-child(3) { width: 30%; }
td {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
확인: 긴 텍스트가 든 셀이 표를 옆으로 밀어내지 않고 …로 잘리는가.
함정 — overflow 셋만 주면 아무 효과가 없다. 표에 width를 주고, 표의 table-layout: fixed나 셀의 max-width: 0 중 하나는 있어야 한다(둘 다는 필요 없다 — Chrome 156·Firefox 150). 표 width 없이 max-width: 0만 주면 열이 0으로 접힌다.
/* ❌ 아무 효과 없음 */
td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
자동 레이아웃의 셀은 내용 너비만큼 늘어나서 overflow가 잘라낼 경계가 없다. fixed는 열 너비를 표·col 너비로만 정해 내용이 끼어들 자리를 없애고1, max-width: 0은 자동 레이아웃에서 셀이 내용 기반 너비를 주장하지 못하게 한다 — 다만 표 셀의 max-width는 CSS 2.1이 정의하지 않은 동작이라 브라우저 관행에 기댄다2.
컬럼별 선택적 적용
td:not(.col-action) {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td.col-action {
white-space: nowrap;
}
셀 안에 복합 요소
버튼/배지 등이 텍스트와 함께 있을 때 inner wrapper에 위임.
td {
max-width: 0;
}
td > div {
display: flex;
align-items: center;
gap: 6px;
overflow: hidden;
}
td > div > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
React 컴포넌트 패턴
TanStack Table size 옵션과 조합.
const columns = [
columnHelper.accessor('name', {
size: 200,
cell: ({ getValue }) => (
<span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{getValue()}
</span>
),
}),
]
<table style={{ tableLayout: 'fixed', width: '100%' }}>
<colgroup>
{table.getFlatHeaders().map(header => (
<col key={header.id} style={{ width: header.getSize() }} />
))}
</colgroup>
...
</table>
재사용 셀 래퍼:
function EllipsisCell({ children }: { children: React.ReactNode }) {
return (
<td style={{ maxWidth: 0 }}>
<div style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{children}
</div>
</td>
)
}
flexbox / multiline 참고
- Using Flexbox and text ellipsis together · Leonardo Faria3
- Multiline truncated text with “show more” button (with just CSS) - Paul Bakaus’ blog4
- Recreating MDN’s Truncated Text Effect
Footnotes
-
table-layout - MDN: “The horizontal layout doesn’t depend on the contents of the cells because it depends only on explicitly set widths.” · “Cells use the overflow property to determine whether to clip any overflowing content, but only if the table has a known width” ↩
-
CSS 2.1 §10.4: “In CSS 2.1, the effect of ‘min-width’ and ‘max-width’ on tables, inline tables, table cells, table columns, and column groups is undefined.” ↩
-
flexbox | 파일명에 ellipsis 효과 적용, 단 파일 확장자는 제외. ↩
-
:truncated개념이 없기 때문에 ResizeObserver와 영역값을 체크해서 구현. ↩
The clamp() CSS function clamps a value within a range of values between a defined minimum bound and a maximum bound. The function takes three parameters: a minimum value, a preferred value, and a maximum allowed value.
- Clamp | Utopia1
- Linearly Scale font-size with CSS clamp() Based on the Viewport | CSS-Tricks2
- Modern Fluid Typography Using CSS Clamp — Smashing Magazine
Footnotes
A reset stylesheet (or CSS reset) is a collection of CSS rules used to clear the browser’s default formatting of HTML elements, removing potential inconsistencies between different browsers.
- An Interview With Elad Shechter on “The New CSS Reset” | CSS-Tricks1
- Modern CSS Reset / Global Styles2
- Notes on Josh Comeau’s Custom CSS Reset | CSS-Tricks - CSS-Tricks3
- Your CSS reset needs text-size-adjust (probably) | CSS-Tricks - CSS-Tricks4
Footnotes
-
all: unset;을 적용해서 reset 하는 방법을 제시하고 있다. ↩ -
box-sizing, 기본 margin 제거, percent 기반 높이, line-height 조정, font-smoothing, 미디어 타입 기본값, form 요소 글꼴 상속, 줄 바꿈, 루트 스태킹 컨텍스트로 구성된 초기화 ↩
-
Chris Coyier 가 위 Josh Comeau 리셋을 줄마다 평한 글. 브라우저 기본 스타일 차이가 줄어서 요즘 리셋은 “의견이 담긴 기본값 모음”에 가깝다는 관점 —
box-sizing은 상속 방식으로(컴포넌트에서 되돌리기 쉽게),margin: 0이면padding: 0도,-webkit-font-smoothing: antialiased전역 적용엔 반대, 글꼴 크기 기준은body가 아니라html(rem 의 root). ↩ -
text-size-adjust 관련한 상황들 ↩
- Animating with Clip-Path | CSS-Tricks
- Ghost Buttons with Directional Awareness in CSS | CSS-Tricks1
- Chameleonic Header | CodyHouse
- Using “box shadows” and clip-path together | CSS-Tricks2
mask — 현재 개체를 배경으로 합성하기 위한 알파 마스크를 정의
- Masking GIFs with other GIFs | CSS-Tricks
- Going From Solid to Knockout Text on Scroll | CSS-Tricks
- How to Create an Animated Chart of Nested Squares Using Masks | CSS-Tricks
Footnotes
- content-visibility: the new CSS property that boosts your rendering performance1
- More on content-visibility | CSS-Tricks2
- When is it “Right” to Reach for contain and will-change in CSS? - CSS-Tricks3
- contain - CSS: Cascading Style Sheets | MDN
- How I made Google’s data grid scroll 10x faster with one line of CSS | by Johan Isaksson | Oct, 2021 | Medium
Footnotes
Building a multi-select component
다중 선택 UI를 구현하기위해서 checkbox, select 두가지 방법으로 작업하는 방식을 소개하고 있다. 그외 선택된 상태값을 얻기위한 counter() 함수, 모바일 체크를 위한 미디어쿼리도 알려주고 있다. 개수 세기는 JS 없이 CSS 카운터로 된다.1
aside {
counter-reset: filters;
& :checked {
counter-increment: filters;
}
&::after {
content: counter(filters);
}
}
@media (pointer: coarse) {
/* … */
}
Footnotes
-
Using CSS counters - MDN: “Once initialized, a counter’s value can be increased or decreased using counter-increment.” / “The value of a counter can be displayed using either the counter() or counters() function in a content property.” ↩
Under-Engineered Select Menus | Adrian Roselli
font,letter-spacing,word-spacing상속appearance화살표 수정- 상태(focus, required, invalid)에 따른 스타일 추가
표준은 ::placeholder 하나다(2020년부터 전 브라우저).1 아래는 그 전의 접두사와 IE·레거시 Edge 전용 컨트롤.
::-webkit-input-placeholder /* for (Chrome/Safari/Opera) */
:-ms-input-placeholder /* for IE. */
::-ms-input-placeholder /* for Edge (also supports webkit prefix) */
::-ms-clear {}
::-ms-reveal {}
Footnotes
-
::placeholder - MDN: “The ::placeholder CSS pseudo-element represents the placeholder text in an or — Baseline: “It’s been available across browsers since January 2020.” ↩
- Creating websites with prefers-reduced-data | Polypane, The browser for ambitious developers1
- Modifying Specific Letters with CSS and JavaScript | CSS-Tricks2
- Stroke Text CSS: 📕 The Definitive Guide - Coding Dude3
- Use advanced typography with local fonts4
- Leading-Trim: The Future of Digital Typesetting | by Ethan Wang | Microsoft Design | Medium5
- Google Fonts6
- Using Font Variant Numeric7
- Color Fonts - Get ready for the revolution!
- How to Use Color Fonts on the Web
- Deep dive CSS: font metrics, line-height and vertical-align - Vincent De Oliveira
- Font size is useless; let’s fix it
- https://gwfh.mranftl.com/fonts/noto-sans-kr?subsets=korean,latin
폰트 만들기
- https://apps.apple.com/us/app/fontself-make-your-own-fonts/id1512959192
- https://glyphsapp.com/
- https://www.figma.com/community/file/1115382696459820988
- https://www.amygoodchild.com/blog/cursive-handwriting-in-javascript
Footnotes
type Props = {
popover: 'auto' | 'hint' | 'manual'
popovertarget: string
popovertargetaction: 'hide' | 'show' | 'toggle'
}
type State = {
hasBackdrop: boolean // CSS ::backdrop
isPopoverOpen: boolean // CSS :popover-open
}
type Methods = {
hidePopover: () => void
showPopover: () => void
togglePopover: () => void
}
type Events = {
beforetoggle: () => void
toggle: () => void
}
baseColor 위에 흰색을 몇 % 로 덮어야 targetColor 가 되는지 역산한다 — 합성식 결과 = 흰색·α + base·(1−α) 를 α 에 대해 푼 것1
class ColorManipulator {
private baseColor: string
private targetColor: string
constructor(baseColor: string, targetColor: string) {
this.baseColor = baseColor
this.targetColor = targetColor
}
private hexToRgb(hex: string) {
const bigint = parseInt(hex.slice(1), 16)
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255,
}
}
public calculateOpacity() {
const target = this.hexToRgb(this.targetColor)
const baseRG = this.hexToRgb(this.baseColor)
const opacities = [
(target.r - baseRG.r) / (255 - baseRG.r),
(target.g - baseRG.g) / (255 - baseRG.g),
(target.b - baseRG.b) / (255 - baseRG.b),
]
const averageOpacity =
opacities.reduce((sum, value) => sum + value, 0) / opacities.length
return averageOpacity
}
public getCssRGBA() {
const opacity = this.calculateOpacity()
return `rgba(255, 255, 255, ${opacity.toFixed(2)})` // 덮는 색은 흰색
}
}
const manipulator = new ColorManipulator('#000000', '#D1D7DE')
const opacity = manipulator.calculateOpacity()
const cssRGBA = manipulator.getCssRGBA() // rgba(255, 255, 255, 0.84)
채널별 α 가 다르면(여기선 0.82·0.84·0.87) 평균 하나로는 정확히 못 맞춘다 — 검정 위 흰색 0.84 는 rgb(214, 214, 214), 목표는 rgb(209, 215, 222).
Footnotes
-
Compositing and Blending Level 1 §5.1 Simple alpha compositing - W3C: “co = Cs x αs + Cb x αb x (1 - αs)” — 바탕이 불투명(αb = 1)이면
Cs·α + Cb·(1−α). ↩
@value b from "./b.module.css";
.root {
color: aquamarine;
}
.root :global(.b) {
text-decoration: line-through;
}
@value 로 가져온 이름은 선언 값뿐 아니라 셀렉터 안에서도 치환된다1 — 다른 모듈의 클래스 b 를 가져오면 :global(.b) 가 그 모듈의 해시된 클래스(._b_15lvz_1)가 된다. @value 줄이 없으면 문자 그대로 .b 라 아무것도 안 잡힌다.
Footnotes
-
icss-utils
replaceSymbols.js—postcss-modules-values가 모은 정의를 이 함수로 치환하는데, 선언(decl)뿐 아니라 규칙의 셀렉터(rule.selector)도 돈다. ↩
같은 두 시트라도 클라이언트 이동이 <head> 안 순서를 바꾸면 결과가 뒤집힌다.1
- v14.1.4 → v14.2.0 — 이 사이에 CSS 청킹이 바뀌었다 (#63157)
- avoid merging global css in a way that leaks into other chunk groups · #67373 — v15 canary 부터, 14.2.x 엔 없다
- fix(build): fix css modules out of order issue from libraries · #70087 — 열림, webpack 쪽 수정을 기다리는 중
Footnotes
-
Introduction to the CSS cascade — MDN: “if there are competing values for a property that are in style block matching selectors of equal specificity and scoping proximity, the last declaration in the style order is applied.” ↩
// 기본
@primary: #3498db;
.button { color: @primary; }
// 보간 (선택자, 속성, URL에서)
@component: button; // 따옴표를 넣으면 .'button' 으로 나온다
.@{component} { ... } // .button
background-@{property}: blue; // background-color: blue
url('@{path}/icon.svg')
// 변수를 이용한 변수
@theme: 'primary';
color: @@theme; // @primary 값
// 맵 (LESS 3.5+)
@colors: { primary: #3498db; danger: #e74c3c; };
color: @colors[primary];
// CSS 변수 조합
:root { --primary: @primary; }
.el { color: var(~'--@{prefix}-color'); }
~""이스케이프 (문자열 그대로 출력)@{}보간 (변수를 문자열로 치환)@@var변수를 이용한 변수 참조
로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS
스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊긴다. 깜빡임은 양 끝에서 따로 생기는데 CSS는 한쪽만 잡는다. 어느 끝인지는 응답 시각을 붙잡고 보인 시간을 재서 가른다 — DOM에 붙어 있던 시간이 아니다.
흐름
- headless Chrome + Node 전역
WebSocket으로 raw CDP. MCPemulate는 프리셋 스로틀링뿐이라 요청 하나를 못 붙잡는다. - 프로브 — 스피너가 사라질 때 보인 시간을 돌려준다.
- 경계값이면
disabled-by-default-devtools.screenshottrace로 확인한다. 스크린샷은 바뀐 프레임에만 찍히니 처음 물든 장 → 다음 흰 장이 보인 시간이다. - 응답 < 지연인데 보였으면 앞쪽, 응답 > 지연인데 최소 노출보다 짧으면 뒤쪽.
| 지연 | 응답 | 최소 노출 | 프로브 | trace | 판정 |
|---|---|---|---|---|---|
| 0 | 100 | — | 9프레임 · 133ms | 앞쪽 | |
| 300 | 100 | — | 0 | 물든 장 없음 | 앞쪽 억제됨 |
| 300 | 350 | — | 4프레임 · 66ms | 약 48ms, 불투명도 최대 약 0.8 | 뒤쪽 |
| 300 | 350 | 500 | 515ms | 뒤쪽 잡힘 | |
| 300 | 100 | 500 | 0 | 앞쪽 억제 유지 |
실측 조건은 각주에 있다.1 셋째 줄이 뒤쪽 깜빡임의 실제 모습이다 — 페이드가 끝나기 전에 끊겨 한 번도 제대로 진해지지 못하고 사라진다.
처방
- 앞쪽 —
animation-delay/transition-delay. 응답이 지연보다 빠르면 스피너를 통째로 건너뛴다.@starting-style단독(지연 0)으로는 못 막는다. - 뒤쪽 — CSS는 시작만 통제하고 끝은 CSS가 모르는 응답 시각이 정하니, 사라지는 시각을 JS에서 민다(
useMinimumLoading). 최소 노출은 처음 보인 시각부터 잰다: 사라지는 시각 =max(응답 시각, firstVisibleAt + 최소 노출). 요청 시점부터 재면 표 마지막 줄에서 안 떠도 될 스피너를 붙잡는다.
밟은 것
spawn한 프로세스만 죽이면 Chrome 자식이 남아 다음 실행이 옛 페이지에 붙는다 →detached: true+process.kill(-pid).- 옛 페이지에 붙은 채
Runtime.enable하면 이전 console 메시지가 다시 온다. - 포트가 뜨기 전
/json은ECONNREFUSED→ 폴링. - 첫 측정은 느리다(hold 100에 응답 149ms) → 첫 줄은 다시 잰다.
곁가지 — @starting-style은 같은 뿌리의 반대편이다. transition은 두 상태 사이를 보간하는데, 진입에는 옮겨갈 이전 상태가 없고2 퇴장에는 요소 자체가 이미 사라진다. @starting-style은 앞쪽의 출발값을 대신 정해주고(진입 fade-in), useMinimumLoading은 뒤쪽의 unmount를 미룬다. 뿌리는 같아도 손대는 지점이 달라 별개 결정이다.
Footnotes
-
Chrome 154 headless 에서 raw CDP 로 실측 (2026-09-30).
/api요청을Fetch.requestPaused로 100·350ms 붙잡고, 스피너(animation: fade 100ms <지연> both)의opacity를 매requestAnimationFrame마다 읽었다. 프로브를 먼저 걸고 버튼 클릭으로 스피너를 띄운 실행에서도 hold 100 → 0프레임, hold 350 → 2프레임(34ms)으로 같았다. 프로브 대 trace 비교는 스피너를 400px 빨간 상자로 두고Tracing의Screenshot이벤트를 받아, JPEG 를sips로 BMP 로 바꿔 상자 안 한 점의 픽셀을 읽었다 — hold 350 에서(255,199,200)→(255,109,110)→(255,51,52)→ 흰색. ↩ -
@starting-style — MDN: “CSS transitions are by default not triggered on an element’s initial style update, or when its display type changes from none to another value. To enable first-style transitions, @starting-style rules are needed.” ↩
- How to Modify Nodes in an Abstract Syntax Tree | CSS-Tricks1
- AST for JavaScript developers. TL;DR This article is my talk for… | by Bohdan Liashenko | ITNEXT2
- GitHub - NV/CSSOM: Unmaintained! ⚠️ CSS Object Model implemented in pure JavaScript. Also, a CSS parser.3
- GitHub - csstree/csstree: A tool set for CSS including fast detailed parser, walker, generator and lexer based on W3C specs and browser implementations
Footnotes
hash 링크로 연결될 경우 스크롤위치가 최상단으로 위치하기 때문에 문제(헤더가 고정일 경우)가 있을수도 있어서 scroll-margin-top으로 제어가 가능한 부분을 설명하고 있다.
- Add scroll margin to all elements which can be targeted - Piccalilli1
- Fixed Headers and Jump Links? The Solution is scroll-margin-top | CSS-Tricks
- Prevent content from being hidden underneath a fixed header by using scroll-margin-top – Bram.us
Footnotes
-
2ex유닛을 사용하여 선택한 글꼴의 x 높이의 상대적인 크기로 설정. ↩