컴포넌트

Number Field

숫자를 입력하거나 증감 버튼으로 조절합니다.

설치

pnpm dlx ply-ui add number-field icon

공통 설치 설정을 먼저 적용합니다.

사용법

min/max로 허용 범위, step으로 변화량을 지정할 수 있습니다.

value/onValueChange로 값에 반응하고, 증감 버튼에는 의미 있는 aria-label을 넣습니다.

문구 바꾸기

증감 버튼의 기본 이름은 "늘리기"·"줄이기", 입력의 역할 설명은 "숫자 입력"입니다. 무엇을 조절하는지 드러나도록 Increment·Decrement에 aria-label="인원 늘리기"처럼 지정하는 것을 권장합니다. Input의 역할 설명은 aria-roledescription으로 바꿉니다.

구성

구성 요소역할
Root숫자 값, 범위, 증감 단위를 관리합니다.
Group입력과 증감 버튼을 묶습니다.
Input숫자 입력 영역입니다.
Increment / Decrement값을 늘리거나 줄입니다.
ScrubArea드래그로 숫자를 바꿀 수 있는 영역입니다.

범위와 증감 단위

min, max, step을 함께 지정합니다. 이 예제는 0에서 100까지 5씩 조절합니다.

구현 코드

components/ui/number-field.tsx
number-field.tsx
"use client";

import type { ComponentProps } from "react";
import { NumberField as Primitive } from "@base-ui/react/number-field";
import { withClassName } from "../../lib/cx";
import "./number-field.css";

// 동작과 접근성은 Base UI가 담당합니다. 이 파일은 스타일 연결만 담당합니다.
function NumberFieldRoot({
  className,
  ...props
}: ComponentProps<typeof Primitive.Root>) {
  return (
    <Primitive.Root
      {...props}
      className={withClassName("rbx-field", className)}
    />
  );
}

function NumberFieldGroup({
  className,
  ...props
}: ComponentProps<typeof Primitive.Group>) {
  return (
    <Primitive.Group
      {...props}
      className={withClassName("rbx-number-group", className)}
    />
  );
}

// Base UI의 영어 기본 문구를 한국어로 바꿉니다. 같은 속성을 넘기면 그 값을 사용합니다.
function NumberFieldInput({
  className,
  ...props
}: ComponentProps<typeof Primitive.Input>) {
  return (
    <Primitive.Input
      aria-roledescription="숫자 입력"
      {...props}
      className={withClassName("rbx-input", className)}
    />
  );
}

function NumberFieldIncrement({
  className,
  ...props
}: ComponentProps<typeof Primitive.Increment>) {
  return (
    <Primitive.Increment
      aria-label="늘리기"
      {...props}
      className={withClassName("rbx-icon-button", className)}
    />
  );
}

function NumberFieldDecrement({
  className,
  ...props
}: ComponentProps<typeof Primitive.Decrement>) {
  return (
    <Primitive.Decrement
      aria-label="줄이기"
      {...props}
      className={withClassName("rbx-icon-button", className)}
    />
  );
}

function NumberFieldScrubArea({
  className,
  ...props
}: ComponentProps<typeof Primitive.ScrubArea>) {
  return (
    <Primitive.ScrubArea
      {...props}
      className={withClassName("rbx-label", className)}
    />
  );
}

// Root/Portal 등 스타일 없는 파트와 제네릭 API는 원본을 그대로 보존합니다.
export const NumberField = {
  ...Primitive,
  Root: NumberFieldRoot,
  Group: NumberFieldGroup,
  Input: NumberFieldInput,
  Increment: NumberFieldIncrement,
  Decrement: NumberFieldDecrement,
  ScrubArea: NumberFieldScrubArea,
};
components/ui/number-field.css
number-field.css
@import "./field.css";
@layer theme, base, components, utilities;
@layer components.ply-base, components.ply;

@layer components.ply {
  /* NumberField: 공통 구조는 theme.css, 컴포넌트 전용 조정은 아래에 작성합니다. */
  .rbx-number-group {
    display: flex;
    gap: 4px;
  }
  .rbx-number-group .rbx-input {
    text-align: center;
    min-width: 0;
  }
}

목차