Component
Button
작업을 실행하거나 다음 단계로 이동하는 버튼입니다.
Playground
옵션을 조절하며 화면과 사용 코드를 함께 확인하세요.
loading을 켜면 로딩 문구에 적용됩니다.
버튼 안의 내용
버튼의 표현 방식
inverse는 무채색 반전, brand는 브랜드, danger는 위험, info는 정보
variant가 secondary, outline, ghost일 때 배경에 적용됩니다.
양 끝을 둥글게 표시합니다
컨트롤 높이
touch는 모바일에서 높이를 키우고 compact는 PC 높이를 유지합니다
상호작용과 제출을 막습니다
진행 상태를 표시하고 중복 실행을 막습니다
앞에 저장 아이콘을 붙입니다
뒤에 화살표 아이콘을 붙입니다
import { Button } from '@amineslab/ui'
import { Save, ArrowRight } from '@amineslab/ui/icons'
export default function ExamplePreview() {
return ((<Button loadingLabel={"저장 중"} disabled={false} loading={false} size={"md"} density={"touch"} startIcon={null} endIcon={ArrowRight} variant={"primary"} color={"inverse"} opaque={false} rounded={false}>
{"변경사항 저장"}
</Button>))
}Props
@amineslab/ui에서 Button를 가져옵니다.
import { Button } from '@amineslab/ui'Button
HTML <button> 요소의 표준 속성을 그대로 받습니다.
| Name | Type | Default | Description |
|---|---|---|---|
rounded | boolean | false | 버튼 양 끝 또는 아이콘 버튼을 원형으로 표시합니다. |
variant | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'primary' | 버튼의 표현 방식을 정합니다. 위험 의미는 color="danger"로 지정합니다. |
color | 'inverse' | 'brand' | 'danger' | 'info' | 'inverse' | inverse는 테마에 따라 반전되는 무채색, brand는 브랜드, danger는 위험, info는 정보 색상입니다. |
opaque | boolean | false | outline·secondary·ghost에 불투명 표면을 적용해 아래 섹션색의 영향을 없앱니다. primary·link에는 영향이 없습니다. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'icon-xs' | 'icon-sm' | 'icon' | 'icon-lg' | 'icon-xl' | 'md' | PC 높이 24/28/32/40/48px, 모바일은 각각 +8px입니다. xs는 작은 글자, sm·md·lg는 같은 글자, xl은 큰 글자를 사용합니다. |
density | 'touch' | 'compact' | 'touch' | 터치 배치와 조밀한 배치의 버튼 높이를 전환합니다. |
disabled | boolean | false | 클릭과 키보드 실행, 폼 제출을 막습니다. |
loading | boolean | false | 진행 상태를 표시하고 중복 실행을 막습니다. asChild와는 함께 쓸 수 없습니다. |
loadingLabel | string | - | loading 상태를 보조 기술에 알릴 이름입니다. |
asChild | boolean | false | 단일 자식 요소에 버튼 스타일을 적용합니다. loading, startIcon, endIcon와는 함께 쓸 수 없습니다. |
startIcon | ButtonIcon | - | 왼쪽 아이콘 컴포넌트 참조 또는 ReactNode. null이면 생략합니다. xs 12px·sm/md/lg 16px·xl 20px 아이콘, 간격 0/2/2/4/4px, 아이콘 유무와 관계없이 좌우 여백은 동일합니다. |
endIcon | ButtonIcon | - | 오른쪽 아이콘 컴포넌트 참조 또는 ReactNode. null이면 생략합니다. xs 12px·sm/md/lg 16px·xl 20px 아이콘, 간격 0/2/2/4/4px, 아이콘 유무와 관계없이 좌우 여백은 동일합니다. |
Examples
자주 쓰는 조합을 살펴보고, 필요한 예시의 코드를 펼쳐 확인하세요.
Rounded
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { Button, IconButton } from '@amineslab/ui'
import { Settings, Save } from '@amineslab/ui/icons'
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
<Button rounded>계속</Button>
<IconButton rounded icon={Settings} label="설정"/>
<Button rounded startIcon={Save} aria-label="저장"/>
</ExampleRow>))
}Variants
한 영역에는 primary를 하나만 두고 나머지는 낮은 강조로 배치합니다.
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { type ButtonVariant, Button } from '@amineslab/ui'
const actionVariants = [
'primary',
'secondary',
'outline',
'ghost',
'link',
] as const satisfies readonly ButtonVariant[];
const variants = actionVariants;
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
{variants.map((variant) => (<Button key={variant} variant={variant}>
{variant}
</Button>))}
</ExampleRow>))
}Variant × Color
표현 방식과 색상 의미를 독립적으로 선택합니다. 위험 액션은 variant="destructive" 대신 color="danger"를 사용합니다.
primary
secondary
outline
ghost
link
import { type ButtonVariant, type ButtonColor, Button } from '@amineslab/ui'
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
const actionColors = [
'inverse',
'brand',
'danger',
'info',
] as const satisfies readonly ButtonColor[];
const colors = actionColors;
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
const actionVariants = [
'primary',
'secondary',
'outline',
'ghost',
'link',
] as const satisfies readonly ButtonVariant[];
const variants = actionVariants;
export default function ExamplePreview() {
return ((<div className="mx-auto grid w-fit max-w-full gap-5">
{variants.map((variant) => (<div key={variant} className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-center gap-3">
<p className="m-0 text-body-3 text-text-secondary">{variant}</p>
<ExampleRow className="justify-start">
{colors.map((color) => (<Button key={color} variant={variant} color={color}>
{color}
</Button>))}
</ExampleRow>
</div>))}
</div>))
}Background
elevation과 색상 토큰은 그대로 유지합니다. true는 그 아래에 현재 테마의 불투명 바탕을 추가해 섹션색이 비치지 않도록 합니다. hover·active에도 같은 바탕을 유지합니다.
opaque=false
outline
secondary
ghost
opaque=true
outline
secondary
ghost
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { type ButtonColor, Button } from '@amineslab/ui'
const actionColors = [
'inverse',
'brand',
'danger',
'info',
] as const satisfies readonly ButtonColor[];
const colors = actionColors;
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
const surfaceVariants = ['outline', 'secondary', 'ghost'] as const;
export default function ExamplePreview() {
return ((<div className="grid w-full gap-4">
{[false, true].map((opaque) => (<div key={String(opaque)} className="grid gap-4 rounded-section bg-elevation-info-300 p-4">
<div className="mx-auto grid w-fit max-w-full gap-4">
<p className="m-0 text-body-2 text-text-primary">opaque={String(opaque)}</p>
{surfaceVariants.map((variant) => (<div key={variant} className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-center gap-3">
<p className="m-0 text-body-3 text-text-secondary">{variant}</p>
<ExampleRow className="justify-start">
{colors.map((color) => (<Button key={color} variant={variant} color={color} opaque={opaque}>
{color}
</Button>))}
</ExampleRow>
</div>))}
</div>
</div>))}
</div>))
}Sizes
PC 높이 24/28/32/40/48px, 모바일은 +8px. 좌우 여백 4/6/8/10/12px. sm·md·lg는 동일한 글자 크기를 사용합니다.
import { type ControlSize, Button, IconButton, Input, NativeSelect } from '@amineslab/ui'
import { Settings } from '@amineslab/ui/icons'
const controlSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const satisfies readonly ControlSize[];
const sizes = controlSizes;
export default function ExamplePreview() {
return ((<div className="grid w-full gap-4">
{sizes.map((size) => (<div key={size} className="flex flex-wrap items-center gap-2">
<span className="w-6 text-body-3">{size}</span>
<Button size={size}>저장</Button>
<IconButton size={size} icon={Settings} label={`설정 ${size}`}/>
<Input size={size} aria-label={`이름 ${size}`} placeholder="이름" className="w-28"/>
<NativeSelect size={size} aria-label={`종류 ${size}`} wrapperClassName="w-28">
<option>종류</option>
</NativeSelect>
</div>))}
</div>))
}With icons
children에는 레이블을, startIcon·endIcon에는 아이콘 컴포넌트를 넣습니다. 버튼이 xs 12px·sm/md/lg 16px·xl 20px 아이콘 크기와 접근성 처리를 담당합니다. 아이콘 유무와 관계없이 좌우 여백은 동일합니다.
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { Button } from '@amineslab/ui'
import { Save, ArrowRight, Trash2 } from '@amineslab/ui/icons'
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
<Button startIcon={Save}>저장</Button>
<Button endIcon={ArrowRight} variant="outline">
다음
</Button>
<Button startIcon={Trash2} color="danger">
삭제
</Button>
</ExampleRow>))
}양쪽 아이콘과 ReactNode
컴포넌트 참조와 JSX, 이모지를 섞어 쓸 수 있습니다. null·false·빈 Fragment는 슬롯과 여백을 만들지 않습니다. 슬롯은 장식용이므로 의미는 children에 담고, 아이콘만 있는 액션은 IconButton을 사용합니다.
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { Button } from '@amineslab/ui'
import { Save, ArrowRight } from '@amineslab/ui/icons'
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
<Button startIcon={Save} endIcon={ArrowRight}>
저장 후 계속
</Button>
<Button startIcon="✨" endIcon={<ArrowRight />}>
새 기능 살펴보기
</Button>
<Button startIcon={null}>아이콘 없음</Button>
</ExampleRow>))
}크기별 아이콘 여백
xs / sm / md / lg / xl의 기본 좌우 여백은 4 / 6 / 8 / 10 / 12px입니다. 아이콘 유무와 관계없이 양쪽에 같은 여백을 적용하며 PC·모바일, touch·compact에 같은 규칙을 사용합니다. 로딩 스피너도 여백을 바꾸지 않습니다.
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { type ControlSize, Button } from '@amineslab/ui'
import { Save, ArrowRight } from '@amineslab/ui/icons'
const controlSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const satisfies readonly ControlSize[];
const sizes = controlSizes;
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
{sizes.map((size) => (<Button key={size} size={size} startIcon={Save} endIcon={ArrowRight}>
저장 {size}
</Button>))}
</ExampleRow>))
}States
import { type ComponentPropsWithoutRef } from 'react'
import { cn } from '@amineslab/ui/utils'
import { Button } from '@amineslab/ui'
function ExampleRow({ className, stretch = false, ...props }: ComponentPropsWithoutRef<'div'> & {
stretch?: boolean;
}) {
return (<div {...props} className={cn('flex flex-wrap justify-center gap-3', stretch ? 'items-stretch' : 'items-center', className)}/>);
}
export default function ExamplePreview() {
return ((<ExampleRow>
<Button disabled>비활성</Button>
<Button loading loadingLabel="저장하는 중">
저장
</Button>
</ExampleRow>))
}As link
이동만 하는 요소는 asChild로 실제 link를 렌더합니다.
import { Button } from '@amineslab/ui'
export default function ExamplePreview() {
return ((<Button asChild variant="link">
<a href="/components/icon-button">IconButton 문서로</a>
</Button>))
}사용 기준
- 한 영역에서 primary 버튼은 가장 중요한 작업 하나에만 사용합니다.
- 페이지 이동에는 button 대신 링크를 사용하세요.
접근성
화면에 맞는 이름을 제공하고, 키보드만으로 작업을 마칠 수 있는지 확인하세요.
기본 button: Enter 또는 Space로 작업 실행asChild: 자식 요소의 기본 키보드 동작을 따름(예: link는 Enter)