GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

코드 블록 UI 9종 — 하이라이터 없이 붙여 쓰는 코드 창

코드 블록은 문서 안에 프로그래밍 언어를 그대로 옮겨 담은 상자이고, 아홉 가지 모두 복사·전환·펼침·타이핑 같은 동작이 실제로 붙어 있는 화면입니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

복사 버튼(01)은 코드 창에 딱 하나만 붙는 가장 작은 부품이라 먼저 옵니다. 탭(02)과 파일 이름 창 머리(06)는 같은 창 안에서 여러 벌을 오가는 부품이라 그다음이고, 줄 번호(03)와 diff(04)는 코드 자체를 읽는 방식을 바꾸는 부품이라 가운데 놓았습니다. 접고 펴기(05)는 화면 밖으로 나가는 분량을 다루고, 터미널(07)과 코드·결과 전환(08)은 코드가 실행된 뒤를 보여 주는 부품이라 뒤쪽에 두었습니다. 마지막 인라인 팝오버(09)는 코드 창 밖, 본문 문장 속으로 들어간 것이라 가장 나중입니다. 코드 블록 만들기에서 진짜 어려운 자리는 문법 색칠이 아니라, 읽는 사람이 코드를 실제로 가져가는 순간을 어디에 두느냐입니다. 임베드 축에서 재생 단추까지 직접 만드는 이웃은 동영상 플레이어 만들기이고, 사이드바에 쌓이는 목록형은 알림 목록 ui입니다. 줄 번호는 세 자리를 넘어갈 때 폭을 다시 계산하도록 했고, 짚어 주는 줄이 두 줄 이상이면 위쪽 줄에만 색을 진하게 남겼습니다. 복사 버튼의 체크 표시는 1.2초 뒤에 원래 모양으로 돌아오게 정했습니다.

01누르면 복사됐다고 알려 주는 버튼

무엇이 보이나

오른쪽 위 버튼을 누르면 Clipboard 문서의 navigator.clipboard.writeText 가 코드 문자열을 통째로 넘겨받고, 라벨은 트랜지션 없이 한 프레임 컷으로 "복사"에서 "복사됨"으로 바뀝니다.

누르면 복사됐다고 알려 주는 버튼
새 창에서 열기

01 · 누르면 복사됐다고 알려 주는 버튼 — 측정 누적면적 28.962% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

navigator.clipboardaria-livesteps(1, end)

코드에서 볼 곳

같은 문구가 aria-live="polite" 칸에도 함께 적혀 화면을 보지 않는 사람도 복사가 끝난 순간을 알 수 있습니다.

btn.addEventListener('click', function () {
  root.classList.remove('is-demo');
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(codeText.textContent).catch(function () {});
  }
  btn.classList.add('is-copied');
  live.textContent = '복사됨';
  clearTimeout(timer);
  timer = setTimeout(function () {
    btn.classList.remove('is-copied');
    live.textContent = '';
  }, 1600);
});

02같은 예제를 언어별로 바꾸는 탭

코드 스니펫 탭은 화면에서 어떻게 보이나

CSS·SCSS·Tailwind 세 탭 아래 깔린 막대는 클릭할 때마다 getBoundingClientRect 로 눌린 탭의 폭과 왼쪽 좌표를 다시 재서 translateX·scaleX 값으로 바꿔 넣습니다.

같은 예제를 언어별로 바꾸는 탭
새 창에서 열기

02 · 같은 예제를 언어별로 바꾸는 탭 — 측정 누적면적 2.667% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateXaria-selectedtablist

코드 읽는 법

탭 폭을 숫자로 미리 정해 두지 않고 매번 재기 때문에 라벨 글자 수가 달라져도 막대가 옆 탭을 침범하지 않습니다.

function moveIndicator(tab) {
  var tabsRect = tablist.getBoundingClientRect();
  var r = tab.getBoundingClientRect();
  var base = tabs[0].getBoundingClientRect().width;
  var left = r.left - tabsRect.left;
  var scale = r.width / base;
  indicator.style.transform =
    'translateX(' + left + 'px) scaleX(' + scale + ')';
}

03줄 번호와 짚어 주는 줄

어떤 움직임인가

줄 번호는 <span>에 타이핑된 글자가 아니라 counter-increment 로 세어 ::before 에 찍히므로, 코드를 전체 선택해 복사해도 번호는 따라오지 않습니다.

줄 번호와 짚어 주는 줄
새 창에서 열기

03 · 줄 번호와 짚어 주는 줄 — 측정 누적면적 14.751% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

counter-increment::beforeuser-select

어디를 바꾸면 되나

짚어 둔 줄에 포인터를 올리거나 탭으로 초점을 주면 aria-describedby 로 연결된 쪽지가 뜹니다.

.lines { counter-reset: ln; display: block; }
.ln {
  position: relative;
  padding-left: 24px;
  white-space: pre;
}
.ln::before {
  counter-increment: ln;
  content: counter(ln);
  position: absolute;
  left: 0;
  width: 16px;
  text-align: right;
  user-select: none;
}

04더한 줄과 뺀 줄을 나눠 보는 diff

무엇이 보이나

더한 줄은 왼쪽에 초록 띠, 뺀 줄은 붉은 띠를 두르고 그 색은 +·- 기호 칸에도 같이 입혀 색과 기호가 같은 정보를 두 번 말합니다. 기호는 코드 본문과 다른 <span> 안에 있어 실제로 선택해 복사하면 부호 없이 코드만 붙습니다.

더한 줄과 뺀 줄을 나눠 보는 diff
새 창에서 열기

04 · 더한 줄과 뺀 줄을 나눠 보는 diff — 측정 누적면적 28.963% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-expandedborder-inline-startsteps(1, end)

코드에서 볼 곳

가운데 안 바뀐 두 줄은 grid-template-rows 로 접혀 있다가 버튼을 눌러야 펴집니다.

.cd__line--add {
  border-inline-start: 2px solid $color;
  background: rgba($color, .16);
}
.cd__line--del {
  border-inline-start: 2px solid $error;
  background: rgba($error, .18);
}
.cd__fold-body {
  display: grid;
  grid-template-rows: 0fr;
  overflow: hidden;
  transition: grid-template-rows $duration $easing;
}
.cd.is-open .cd__fold-body { grid-template-rows: 1fr; }

05길면 접었다가 펴는 코드 창

화면에서 일어나는 일

긴 파일은 grid-template-rows 를 0fr에서 1fr로 옮겨 여닫으므로, 자바스크립트로 콘텐츠 높이를 재서 px 로 박아 둘 필요가 없습니다.

길면 접었다가 펴는 코드 창
새 창에서 열기

05 · 길면 접었다가 펴는 코드 창 — 측정 누적면적 31.724% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-rowsaria-expandedlinear-gradient

코드 읽는 법

접힌 동안에는 바닥에 흐려지는 띠가 깔려 아래에 더 있다는 사실이 눈에 보입니다.

.ecb__more { display: grid; grid-template-rows: 0fr; }
.ecb.is-open .ecb__more { grid-template-rows: 1fr; }

.ecb__fade {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 32px;
  background: linear-gradient(
    to bottom, rgba(255, 247, 230, 0), $subject-cream
  );
  pointer-events: none;
}
.ecb.is-open .ecb__fade { opacity: 0; }

06파일 이름이 붙은 창 머리

어떤 움직임인가

창 머리 왼쪽엔 신호등 점 세 개와 파일 아이콘, 그리고 지금 보고 있는 파일 이름이 나란히 앉습니다.

파일 이름이 붙은 창 머리
새 창에서 열기

06 · 파일 이름이 붙은 창 머리 — 측정 누적면적 5.082% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-selectedborder-radiustranslateY

어디를 바꾸면 되나

탭을 눌러 파일을 바꾸면 그 이름표가 textContent 로 다시 쓰여 항상 켜진 탭과 같은 값을 보여 주므로, 머리와 탭이 서로 다른 파일을 가리키는 일이 없습니다.

function fwSelect(i) {
  root.classList.remove('is-demo');
  titleEl.textContent = tabs[i].textContent;
  tabs.forEach(function (t, idx) {
    var on = idx === i;
    t.setAttribute('aria-selected', on ? 'true' : 'false');
    t.tabIndex = on ? 0 : -1;
  });
  panels.forEach(function (p, idx) {
    var on = idx === i;
    p.classList.toggle('is-active', on);
    p.setAttribute('aria-hidden', on ? 'false' : 'true');
  });
}

07명령이 스스로 쳐지는 터미널

무엇이 보이나

한 글자씩 36ms 간격의 setTimeout 으로 명령 문자열을 잘라 붙이고, 다 쳐지면 240ms와 520ms 뒤에 출력 두 줄이 차례로 떠오릅니다.

명령이 스스로 쳐지는 터미널
새 창에서 열기

07 · 명령이 스스로 쳐지는 터미널 — 측정 누적면적 2.603% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

steps(24, end)animation-fill-modemonospace

코드에서 볼 곳

커서는 명령 글자와 별도 요소라 폭이 늘어나는 동안 저절로 줄 끝을 따라갑니다.

function play() {
  cmd.textContent = '';
  cmd.style.width = '0ch';
  var i = 0;
  (function tick() {
    i += 1;
    cmd.textContent = CMD.slice(0, i);
    cmd.style.width = i + 'ch';
    if (i < CMD.length) {
      timers.push(setTimeout(tick, 36));
    } else {
      timers.push(setTimeout(function () { show(out1, true); }, 240));
      timers.push(setTimeout(function () { show(out2, true); }, 520));
    }
  })();
}

08코드와 결과를 번갈아 보는 창

화면에서 일어나는 일

같은 창 안 두 겹 중 코드 쪽이 꺼지는 순간 결과 쪽이 opacity·visibility 로 한 프레임에 켜져, 두 장이 겹쳐 반투명하게 비치는 순간이 없습니다.

코드와 결과를 번갈아 보는 창
새 창에서 열기

08 · 코드와 결과를 번갈아 보는 창 — 측정 누적면적 11.172% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-pressedsteps(1, end)visibility

코드 읽는 법

지금 켜진 쪽은 버튼의 aria-pressed 에도 남아 두 버튼 중 무엇이 눌려 있는지 헷갈리지 않습니다.

function setView(showResult) {
  root.classList.remove('is-demo');
  codeBtn.setAttribute('aria-pressed', String(!showResult));
  eyeBtn.setAttribute('aria-pressed', String(showResult));
  codeFace.style.opacity = showResult ? '0' : '1';
  codeFace.style.visibility = showResult ? 'hidden' : 'visible';
  resultFace.style.opacity = showResult ? '1' : '0';
  resultFace.style.visibility = showResult ? 'visible' : 'hidden';
  codeFace.setAttribute('aria-hidden', String(showResult));
  resultFace.setAttribute('aria-hidden', String(!showResult));
}

09낱말을 짚으면 뜻이 뜨는 인라인 코드

어떤 움직임인가

문장 속 border-radius, box-shadow 같은 낱말에는 점선 밑줄만 그어 두고, 올리거나 탭으로 초점을 주면 그 뜻을 적은 쪽지가 문단 아래에서 떠오릅니다.

낱말을 짚으면 뜻이 뜨는 인라인 코드
새 창에서 열기

09 · 낱말을 짚으면 뜻이 뜨는 인라인 코드 — 측정 누적면적 10.227% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-describedbytext-decorationfocus-visible

어디를 바꾸면 되나

쪽지 두 장은 같은 자리에 겹쳐 있다가 짚은 낱말이 바뀌면 글자만 컷으로 갈아 끼웁니다.

.tp__body {
  position: relative;
  padding: $sp-3;
  padding-bottom: $sp-10;
}
.tok {
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}
.tp__pop {
  position: absolute;
  left: $sp-3; right: $sp-3; bottom: $sp-3;
  opacity: 0;
  visibility: hidden;
}
.tp__pop.is-open { opacity: 1; visibility: visible; }

어디서 깨지는가 — 함정

02의 탭 인디케이터 폭은 짐작이 아니라 Playwright 로 getBoundingClientRect 를 세 탭 각각에 대고 잰 값입니다. CSS 탭이 기준(43.2px)이고, 그 오른쪽에 SCSS 탭이 43.2px 지점에서 49.38px 폭으로, Tailwind 탭이 92.58px 지점에서 67.05px 폭으로 이어 붙습니다. 이 세 숫자를 그대로 translateX·scaleX 키프레임에 넣지 않으면 막대가 글자보다 짧거나 길게 걸칩니다.

08의 코드·결과 전환 창은 카드 전체에 overflow: hidden 이 걸려 있어 모서리가 둥글게 잘립니다. 문제는 이 규칙이 안쪽 코드 줄에도 그대로 적용된다는 점인데, 무대 높이(132px)를 넘는 줄은 스크롤바 없이 조용히 잘려 나가고 문서 전체 폭·높이는 그대로 안 넘치므로 자동 검사로는 안 걸립니다. poster.png 를 직접 눈으로 열어 넷째 줄까지 다 보이는지 확인해야 잡히는 함정입니다.

05는 원래 창이 아직 펼쳐진 63% 지점에서 버튼 라벨을 「더 보기」로 되돌렸는데, 그러면 화면에는 코드가 다 보이는데 버튼만 접혀 있다고 화면이 거짓말을 하는 순간이 생겼습니다. 라벨과 화살표를 접힘이 거의 끝나는 84%로 옮겨 grid-template-rows 가 0fr에 가까워지는 시점과 맞춘 뒤에야 없어졌습니다.

09의 설명 쪽지는 position: absolute 로 띄워 두어 부모 높이 계산에 안 잡힙니다. 그래서 본문 아래에 쪽지 한 장 높이만큼 padding-bottom 을 미리 비워 두지 않으면, 좁은 화면에서 쪽지가 카드 바깥으로 넘칩니다. 창 안 글자가 언어마다 길어질 때를 대비하려면 홈페이지 다국어 처리 검수의 점검이 앞서야 합니다. 압축 파일 비밀번호는 8j8vmbbq 이며, 아홉 벌 모두 같은 이유로 좁은 화면 규칙을 한 번씩 다시 확인했습니다.

접근성

아홉 개 모두 prefers-reduced-motion: reduce 에서 자동 루프만 꺼지고, 그 설정에서 화면에 남는 정지 상태는 항목마다 다릅니다. 어떤 값이 정확히 무엇을 끄는지는 MDN 문서에 정리되어 있습니다. 같은 갈래의 다른 화면은 버튼 상태 피드백과 CSS 탭 메뉴 전환에도 있고, 코드처럼 글을 담는 화면은 본문 카테고리에 모여 있습니다.

항목 reduced-motion 정지 화면 키보드 낭독기
01 복사 버튼 "복사됨" 라벨로 고정, 커서 숨김 Tab · Enter aria-live로 복사됨 통지
02 언어 탭 CSS 탭 그대로 고정, 순환 없음 방향키로 탭 전환 aria-selected · role=tablist
03 줄 번호 짚은 줄 배경만 남고 쪽지·커서 숨김 Tab으로 짚은 줄에 초점 aria-describedby로 쪽지 연결
04 diff 접힌 줄 그대로, 추가·삭제 띠는 이미 다 그려짐 접기 버튼 Tab · Enter aria-expanded로 펼침 여부
05 접고 펴기 접힌 「더 보기」 상태로 고정 버튼 Tab · Enter aria-expanded · 라벨 텍스트
06 창 머리 두 번째 탭(style.css)이 선택된 채로 고정 방향키로 탭 전환 aria-selected + 머리 파일명 동기화
07 터미널 명령·출력 두 줄 전부 표시, 커서 깜빡임만 정지 Enter · Space로 재생 role="button" aria-label="다시 재생"
08 코드·결과 전환 코드 면 고정, 결과 면 숨김 버튼 Tab · Enter aria-pressed로 켜진 면 표시
09 인라인 팝오버 쪽지 둘 다 숨김, 밑줄만 남음 Tab으로 낱말 초점, Esc로 닫기 aria-describedby로 쪽지 연결

FAQ

코드 하이라이트 라이브러리 없이 색을 칠하는 게 가능한가요?

여기 아홉 개는 전부 <span class="k">, <span class="s"> 처럼 손으로 나눠 놓은 조각에 색을 입힌 것이라 라이브러리가 없어도 됩니다. 다만 이 방식은 예제가 고정돼 있을 때만 통하고, 사용자가 아무 코드나 붙여 넣어 실시간으로 칠해야 하는 화면이라면 Prism이나 Shiki 같은 전용 도구가 낫습니다. 보여 주는 데서 그치지 않고 고치는 창까지 가려면 코드 에디터 만들기가 다음 단계입니다.

복사 버튼의 navigator.clipboard 가 모든 브라우저에서 되나요?

writeText 는 HTTPS 같은 보안 컨텍스트에서만 열리고, 지원 범위는 caniuse.com 에서 브라우저별로 확인할 수 있습니다. 01의 코드는 navigator.clipboard 자체가 없는 환경이면 if 문에서 조용히 넘어가도록 짜여 있어, 지원하지 않는 브라우저에서도 버튼이 에러로 멈추지는 않습니다.

이 아홉 개를 React로 옮기면 뭐가 달라지나요?

zip 안에는 지금 본 바닐라 판과 같은 동작의 React 판이 함께 들어 있어, classList.toggle 자리가 useState 로, textContent 를 바꾸던 자리가 상태값을 그대로 렌더링하는 자리로 바뀝니다. aria-selected·aria-expanded 같은 접근성 속성은 이름과 값이 그대로 유지되므로 스크린리더 쪽 동작은 옮긴 뒤에도 같습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.