React

Pressed Scale

커스텀 컴포넌트에 SEED의 눌림 축소 효과를 적용하는 방법을 알아봅니다.

SEED 컴포넌트는 눌렸을 때 요소가 살짝 줄어드는 피드백을 기본으로 제공합니다. 직접 만든 컴포넌트에도 같은 효과를 적용할 수 있습니다.

배율이 정해지는 방식

SEED는 축소 배율을 고정값으로 정하지 않습니다. 대신 크기가 다른 요소도 눌렸을 때 비슷한 거리만큼 줄어들도록, 배율을 요소의 실제 크기에서 계산합니다.

요소높이 × 폭계산된 배율실제 축소
아이콘 버튼32 × 320.9382px
텍스트 버튼48 × 1200.958세로 2px, 가로 5px
가로로 긴 버튼48 × 3430.977세로 1.1px, 가로 8px
작은 버튼22 × 220.9171.8px

배율은 렌더된 크기에서 계산되므로 요소가 예상과 다른 크기로 그려져도 — 폰트 스케일링, 화면 폭, 사용자 콘텐츠의 줄바꿈 등 — 별도로 값을 조정할 필요가 없습니다. 계산에 쓰이는 상수는 모든 SEED 요소에 공통으로 적용되며 커스터마이즈 대상이 아닙니다.

적용하기

크기를 측정해 배율을 발행하는 일은 usePressScale이 담당하고, 그 배율을 언제 어떤 요소에 적용할지는 직접 작성합니다. 눌림 스타일은 보통 배경색 변화와 함께 걸리는데, 둘을 하나의 규칙으로 묶어두면 서로 다른 조건에서 각각 동작하도록 나눌 수 없기 때문입니다.

요소 크기 발행하기

usePressScale이 돌려주는 pressScaleRefpressScaleClassName같은 요소에 전달합니다.

MyButton.tsx
import { usePressScale } from "@seed-design/react";
import clsx from "clsx";

function MyButton({ className, ...props }) {
  const { pressScaleRef, pressScaleClassName } = usePressScale();

  return (
    <button
      ref={pressScaleRef} 
      className={clsx(pressScaleClassName, className)} 
      {...props}
    />
  );
}

ref는 요소의 크기를 측정해 발행하고, 클래스는 그 크기에서 배율을 계산하는 규칙을 켭니다. 둘 중 하나라도 빠지면 배율이 계산되지 않고 축소가 일어나지 않습니다.

배율 사용하기

배율은 --seed-press-scale, 트랜지션은 --seed-press-scale-transition으로 발행됩니다. 눌림을 감지하는 선택자에 배율을 적용하세요.

MyButton.tsx
<button
  ref={pressScaleRef}
  className={clsx(
    pressScaleClassName,
    "bg-bg-brand-solid p-x3 rounded-r2",
    "[transition:background-color_0.2s,var(--seed-press-scale-transition)]", 
    "active:[scale:var(--seed-press-scale,1)]", 
  )}
/>

scale-* 유틸리티는 Tailwind CSS 3에서 transform으로, 4에서 scale 속성으로 컴파일되므로 버전에 따라 결과가 다릅니다. 위처럼 arbitrary property 문법([scale:...])을 사용하면 두 버전에서 동일하게 동작합니다.

CSS를 직접 작성할 때는 var(--seed-press-scale, 1)폴백 값 1을 반드시 함께 적어야 합니다. 아직 크기가 측정되지 않은 상태에서 폴백 없이 사용하면 scalenone으로 계산되어, 눌리는 순간 요소가 stacking context에서 벗어납니다.

@seed-design/css/press-scalepressScale에는 폴백이 이미 포함되어 있습니다.

두 단계를 모두 적용하면 아래와 같이 동작합니다. 크기가 다른 두 요소가 비슷한 정도로 줄어듭니다.

이미 ref를 사용하고 있다면

pressScaleRef를 다른 ref와 함께 써야 한다면 useComposedRefs로 합성하세요. 렌더할 때마다 새로 만들어지는 함수를 ref로 넘기면 React가 매 렌더마다 ref를 분리하고 다시 연결하기 때문에, 크기 관찰이 매번 재등록됩니다.

MyButton.tsx
import { useComposedRefs } from "@radix-ui/react-compose-refs";

const MyButton = React.forwardRef<HTMLButtonElement, MyButtonProps>(
  ({ className, ...props }, ref) => {
    const { pressScaleRef, pressScaleClassName } = usePressScale();

    return (
      <button
        ref={useComposedRefs(pressScaleRef, ref)} 
        className={clsx(pressScaleClassName, className)}
        {...props}
      />
    );
  },
);

자동으로 처리되는 것

아래 두 가지는 별도로 작성할 필요가 없습니다.

  • 동작 줄이기 설정: prefers-reduced-motion: reduce 환경에서는 배율이 항상 1로 계산됩니다.
  • 크기를 알 수 없는 상황: JavaScript가 실행되지 않았거나 첫 측정 전이라면 배율이 폴백 값 1로 떨어집니다. 잘못된 배율이 적용되는 대신 효과만 나타나지 않습니다.

알아둘 점

개별 scale 속성

SEED 컴포넌트는 축소를 transform: scale()이 아니라 개별 transform 속성인 scale로 적용합니다. 커스텀 컴포넌트에서도 scale을 사용하세요.

transform은 shorthand라서 한 요소에 하나의 값만 가집니다. 축소를 여기에 적으면 그 요소의 이동이나 회전을 같은 값에 함께 써야 하고, 변형을 추가할 때마다 축소 값을 다시 챙겨야 합니다. 개별 scaletranslate, rotate와 독립적으로 동작해서 이런 부담이 없고, pressScaleClassName이 이미 scale을 사용하므로 축소가 한 속성에 모입니다.

개별 scale 속성은 Chrome 104, Safari 14.1, Firefox 72부터 지원됩니다. 이보다 낮은 버전에서는 축소만 나타나지 않고 나머지 스타일은 그대로 동작합니다.

중첩된 SEED 컴포넌트

커스텀 컴포넌트가 Checkmark, Radiomark, Switchmark를 감싸는 경우, 바깥 요소와 안쪽 마크가 함께 줄어들어 축소가 이중으로 나타날 수 있습니다. 끄는 방법은 각 컴포넌트 문서의 Pressed Scale 항목에 있습니다.

position: fixed 자손

pressScaleClassName이 붙은 요소는 눌리지 않은 상태에서도 scale: 1을 유지합니다. scale 값이 있는 요소는 stacking context이자 position: fixed 자손의 containing block이 되므로, 이 요소 안에 position: fixed 요소가 있다면 뷰포트가 아니라 이 요소를 기준으로 배치됩니다.

이 기준은 눌리기 전후로 바뀌지 않으므로 누르는 동안 자손이 따라 움직이지는 않습니다. 다만 배치 기준 자체가 뷰포트와 달라지는 것이므로, 이런 자손이 있다면 의도한 위치에 표시되는지 확인하세요.

마우스 환경

SEED 컴포넌트는 마우스 환경에서 hover 시 pressed 색상을 표시하지만, 축소는 실제로 누른 동안에만 적용합니다. 커스텀 컴포넌트에서도 배율은 :active에만 연결하세요. 자세한 내용은 Interaction States 문서를 참고하세요.

프로젝트 전체에서 끄기

배율을 1로 고정하는 규칙을 추가하고, SEED CSS보다 뒤에 로드하세요. 배율을 읽는 모든 곳이 이 변수 하나를 거치므로 SEED 컴포넌트와 커스텀 컴포넌트가 함께 꺼집니다.

press-scale-off.css
.seed-press-scale {
  --seed-press-scale: 1;
}

평상시 scale: 1은 그대로 남으므로 position: fixed 자손의 배치 기준은 달라지지 않습니다.

관련 문서

Last updated on

On this page