Button
클릭하거나 키보드로 작업을 실행하는 버튼입니다.
설치
pnpm dlx ply-ui add button공통 설치 설정을 먼저 적용합니다.
사용법
Button 안에 버튼 이름을 넣습니다. 별도의 자식 컴포넌트는 필요하지 않습니다.
variant로 종류를, size로 크기를 지정합니다. 기본값은 emphasis와 lg입니다.
클릭 동작은 onClick으로 연결합니다. 폼을 제출할 때는 type="submit"을 지정합니다.
종류
variant는 버튼의 배경과 글자 색상을 바꿉니다. 동작과 크기는 바뀌지 않습니다.
Emphasis
variant="emphasis"는 강조 색상으로 주요 작업을 표시합니다. variant를 생략하면 적용됩니다.
Standard
variant="standard"는 중립 색상의 배경을 사용합니다. 강조 버튼과 함께 배치하는 일반 작업에 사용합니다.
Soft emphasis
variant="soft-emphasis"는 옅은 강조 색상의 배경을 사용합니다.
Sub emphasis
variant="sub-emphasis"는 현재 구현에서 over-media와 같은 색상 토큰을 사용합니다.
두 종류의 기본 외형은 같습니다.
Subtle
variant="subtle"는 기본 배경을 투명하게 표시합니다. 마우스를 올리거나 누르면 상태에 따른 배경이 나타납니다.
Utility
variant="utility"는 투명한 배경과 일반 작업의 글자 색상을 사용합니다.
현재 구현에서는 subtle과 배경을 공유하고 글자 색상 토큰을 따로 지정합니다.
Over media
variant="over-media"는 이미지나 영상 위에 배치할 때 사용합니다. 예제에서는 배경 위의 버튼을 확인할 수 있습니다.
Alert
variant="alert"는 삭제와 같은 위험한 작업을 경고 색상으로 표시합니다.
확인 대화상자는 별도로 구성해야 합니다.
Link
variant="link"는 링크 색상의 글자를 사용합니다. 이 속성은 외형만 바꾸며, 버튼에 페이지 이동 기능을 추가하지 않습니다.
크기
size로 높이를 지정합니다. 글자 크기와 좌우 여백도 함께 바뀝니다.
| 값 | 높이 |
|---|---|
xs | 24px |
sm | 32px |
md | 40px |
lg (기본값) | 48px |
비활성
disabled를 지정하면 버튼을 누를 수 없고, 키보드의 탭 이동 대상에서도 제외됩니다.
로딩
처리 상태를 직접 관리하고, 처리 중에는 disabled와 aria-busy를 함께 지정합니다.
별도의 loading 속성은 제공하지 않습니다.
이 예제는 600ms 후 완료되는 동작을 사용합니다. 실제 사용 시 코드의 지연 처리를 API 호출로 교체합니다.
속성
이 컴포넌트에서 추가하거나 제한한 속성입니다. 공통 속성과 이벤트는 Base UI Button 문서를 참고하세요.
구현 코드
components/ui/button.tsx
"use client";
import type { ComponentProps } from "react";
import { Button as Primitive } from "@base-ui/react/button";
import { withClassName } from "../../lib/cx";
import "./button.css";
export type ButtonVariant =
| "emphasis"
| "standard"
| "soft-emphasis"
| "sub-emphasis"
| "subtle"
| "utility"
| "over-media"
| "alert"
| "link";
export type ButtonProps = ComponentProps<typeof Primitive> & {
/** 버튼의 색상과 강조 수준입니다. @defaultValue "emphasis" */
variant?: ButtonVariant;
/** 버튼 높이입니다. @defaultValue "lg" */
size?: "xs" | "sm" | "md" | "lg";
};
/** variant는 색상 역할, size는 높이를 고릅니다. 원본 Base UI props/ref도 전달합니다. */
export function Button({
variant = "emphasis",
size = "lg",
className,
...props
}: ButtonProps) {
return (
<Primitive
{...props}
data-variant={variant}
data-size={size}
className={withClassName("rbx-button", className)}
/>
);
}
components/ui/button.css
/* Roblox의 action 토큰을 variant에 직접 연결합니다. */
.rbx-button[data-variant="emphasis"] {
background-color: var(--rbx-color-action-emphasis-background);
color: var(--rbx-color-action-emphasis-foreground);
border-color: var(--rbx-color-action-emphasis-border);
}
.rbx-button[data-variant="standard"] {
background-color: var(--rbx-color-action-standard-background);
color: var(--rbx-color-action-standard-foreground);
border-color: var(--rbx-color-action-standard-border);
}
.rbx-button[data-variant="soft-emphasis"] {
background-color: var(--rbx-color-action-soft-emphasis-background);
color: var(--rbx-color-action-soft-emphasis-foreground);
border-color: var(--rbx-color-action-soft-emphasis-border);
}
.rbx-button[data-variant="sub-emphasis"] {
background-color: var(--rbx-color-action-sub-emphasis-background);
color: var(--rbx-color-action-sub-emphasis-foreground);
border-color: var(--rbx-color-action-sub-emphasis-border);
}
.rbx-button[data-variant="subtle"] {
background-color: var(--rbx-color-action-subtle-background);
color: var(--rbx-color-action-subtle-foreground);
border-color: var(--rbx-color-action-subtle-border);
}
.rbx-button[data-variant="utility"] {
background-color: var(--rbx-color-action-utility-background);
color: var(--rbx-color-action-utility-foreground);
border-color: var(--rbx-color-action-utility-border);
}
.rbx-button[data-variant="over-media"] {
background-color: var(--rbx-color-action-over-media-background);
color: var(--rbx-color-action-over-media-foreground);
border-color: var(--rbx-color-action-over-media-border);
}
.rbx-button[data-variant="alert"] {
background-color: var(--rbx-color-action-alert-background);
color: var(--rbx-color-action-alert-foreground);
border-color: var(--rbx-color-action-alert-border);
}
.rbx-button[data-variant="link"] {
background-color: var(--rbx-color-action-link-background);
color: var(--rbx-color-action-link-foreground);
border-color: var(--rbx-color-action-link-border);
}
/* Foundation Web size map: label line-height is 100%, not body text's 140%. */
.rbx-button {
height: 48px;
min-height: 48px;
padding: 0 12px;
font: var(--rbx-typography-label-large-font);
white-space: nowrap;
}
.rbx-button[data-size="md"] {
height: 40px;
min-height: 40px;
font: var(--rbx-typography-label-medium-font);
}
.rbx-button[data-size="sm"] {
height: 32px;
min-height: 32px;
padding: 0 8px;
gap: 4px;
font: var(--rbx-typography-label-small-font);
}
.rbx-button[data-size="xs"] {
height: 24px;
min-height: 24px;
padding: 0 8px;
gap: 4px;
font: var(--rbx-typography-label-small-font);
border-radius: 4px;
}
.rbx-button[data-variant="utility"] {
background-color: var(--rbx-color-action-subtle-background);
color: var(--rbx-color-action-standard-foreground);
}
.rbx-button[data-variant="link"] {
text-decoration: none;
color: var(--rbx-color-system-emphasis);
}
/* 원본은 비활성 강조 버튼을 standard 역할로 바꾼 뒤 opacity 0.5를 적용합니다. */
.rbx-button:is(:disabled, [data-disabled]):is(
[data-variant="emphasis"],
[data-variant="standard"],
[data-variant="soft-emphasis"],
[data-variant="alert"]
) {
background-color: var(--rbx-color-action-standard-background);
color: var(--rbx-color-action-standard-foreground);
}