Button
액션을 실행하는 컨트롤입니다. 폼 제출, 저장, 삭제처럼 사용자가 의도적으로 일으키는 동작에 씁니다.
import { Button } from 'neba';
<Button onClick={save}>저장</Button>;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'solid' | 표면의 무게. 채움 / 하이라인 / 없음 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일. xs 22px · sm 26px · md 32px · lg 40px · xl 48px |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음. 호버는 한 단계 올리고, 누르면 한 단계 내립니다 |
| startIcon | ReactNode | — | 라벨 앞에 놓이는 내용. 1.2em으로 그려져 라벨 크기를 따라갑니다 |
| endIcon | ReactNode | — | 라벨 뒤에 놓이는 내용 |
| loading | boolean | false | startIcon 자리에 스피너를 띄우고 활성화를 막습니다. 포커스는 유지됩니다 |
| readOnly | boolean | false | 비활성이되 흐려지지 않음. 액션은 존재하지만 여기서는 쓸 수 없다는 뜻 |
| disabled | boolean | false | 사용 불가. 색 계열을 버리고 중립 회색이 되며, 포커스 순서에서 빠집니다 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| render | useRender.RenderProp | — | button 대신 다른 요소로 렌더링합니다 (<a href>, 라우터의 Link). 링크는 링크로 남아 크롤러와 스크린리더가 그대로 인식합니다 |
| children | ReactNode | — | 라벨. 생략하면 정사각형 아이콘 버튼이 됩니다 |
<button>의 native 속성은 그대로 전달됩니다. color만 위 표의 color와 이름이 겹쳐 제외됩니다.
공통 축(variant size color density elevation)의 의미는 Prop 규약에 있습니다.
예시
variant
solid는 주 액션, outline은 보조 액션, text는 목록이나 툴바에 놓이는 낮은 무게의 액션입니다. 한 화면에 solid는 하나만 두세요.
import { Button } from 'neba';
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="solid">Save</Button>
<Button variant="outline">Cancel</Button>
<Button variant="text">Details</Button>
</div>
);
}color
여섯 가지 역할 색만 받습니다. 임의의 색상값은 지정할 수 없습니다.
import { Button } from 'neba';
const COLORS = ['primary', 'secondary', 'success', 'warning', 'danger', 'info'] as const;
export default function ButtonColors() {
return (
<div className="flex flex-col gap-3">
{(['solid', 'outline', 'text'] as const).map((variant) => (
<div key={variant} className="flex flex-wrap items-center gap-2">
{COLORS.map((color) => (
<Button key={color} variant={variant} color={color}>
{color}
</Button>
))}
</div>
))}
</div>
);
}size
높이와 타입 스케일을 함께 정합니다. xs 22px · sm 26px · md 32px · lg 40px · xl 48px이며, 데스크톱 기본은 md입니다.
import { Button } from 'neba';
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="xs">xs</Button>
<Button size="sm">sm</Button>
<Button size="md">md</Button>
<Button size="lg">lg</Button>
<Button size="xl">xl</Button>
</div>
);
}density
density는 좌우 padding만 바꿉니다. 같은 size라면 높이가 동일하므로 한 줄에 섞어 놓아도 기준선이 맞습니다.
import { Button } from 'neba';
export default function ButtonDensity() {
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-3">
<Button density="default">Save changes</Button>
<Button density="default" variant="outline">
Save changes
</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button density="compact">Save changes</Button>
<Button density="compact" variant="outline">
Save changes
</Button>
</div>
</div>
);
}startIcon과 endIcon
아이콘은 1.2em으로 그려져 라벨 크기를 따라갑니다. 크기를 따로 지정할 필요가 없습니다. children 없이 아이콘만 주면 정사각형 버튼이 되며, 이때는 aria-label이 필요합니다. 아이콘 전용 컨트롤이라면 IconButton이 label을 필수로 요구합니다.
import { Button } from 'neba';
function PlusIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 3.5v9M3.5 8h9" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
</svg>
);
}
function ChevronIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="m6 4 4 4-4 4"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default function ButtonIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button startIcon={<PlusIcon />}>New project</Button>
<Button variant="outline" endIcon={<ChevronIcon />}>
Continue
</Button>
<Button variant="outline" aria-label="Add" startIcon={<PlusIcon />} />
</div>
);
}loading · readOnly · disabled
| prop | 겉모습 | focus | native disabled |
|---|---|---|---|
loading | 그대로. startIcon 자리에 spinner | 유지 | 아니오 |
readOnly | 색은 유지, 평평해지고 채도가 빠짐 | 유지 | 아니오 |
disabled | 색 계열을 버리고 중립 회색 | 빠짐 | 예 |
세 상태 모두 클릭이 부모로 전파되지 않습니다.
import { Button } from 'neba';
export default function ButtonStates() {
return (
<div className="flex flex-col gap-3">
{(['solid', 'outline', 'text'] as const).map((variant) => (
<div key={variant} className="flex flex-wrap items-center gap-2">
<Button variant={variant}>Normal</Button>
<Button variant={variant} loading>
Loading
</Button>
<Button variant={variant} disabled>
Disabled
</Button>
<Button variant={variant} readOnly>
Read-only
</Button>
</div>
))}
</div>
);
}elevation
그림자 깊이입니다. 기본값 0은 그림자가 전혀 없다는 뜻입니다. hover하면 한 단계 올라가고 누르면 한 단계 내려가므로, 0인 버튼도 눌린 것이 표현됩니다.
import { Button } from 'neba';
export default function ButtonElevation() {
return (
<div className="flex flex-wrap items-center gap-4">
<Button elevation={0} size="lg">
elevation 0
</Button>
<Button elevation={1} size="lg">
elevation 1
</Button>
<Button elevation={2} size="lg">
elevation 2
</Button>
<Button elevation={3} size="lg">
elevation 3
</Button>
</div>
);
}fullWidth
컨테이너 너비만큼 확장합니다.
import { Button } from 'neba';
export default function ButtonFullWidth() {
return (
<div className="flex max-w-sm flex-col gap-2">
<Button fullWidth size="lg">
Create workspace
</Button>
<Button fullWidth variant="text" color="secondary">
Maybe later
</Button>
</div>
);
}render
<button> 대신 다른 요소로 렌더링합니다. 누르면 이동하는 액션은 <a href>여야 합니다. 크롤러가 따라갈 수 있고, 스크린리더의 링크 목록에 올라가며, 새 탭으로 열기나 주소 복사 같은 브라우저의 기본 동작이 그대로 살아납니다. 라우터의 Link도 같은 방식으로 넘깁니다.
표면과 크기, press 신호는 그대로입니다. <a>에는 disabled가 없으므로, 사용할 수 없어야 하는 버튼은 <button>으로 두세요.
import { Button } from 'neba';
export default function ButtonRender() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button render={<a href="/guide/getting-started" />}>Get started</Button>
<Button render={<a href="/components/" />} variant="outline" color="secondary">
All components
</Button>
<Button render={<a href="/design/design-language" />} variant="text" size="sm">
Design language
</Button>
</div>
);
}접근성
- 기본적으로 native
<button>으로 렌더링됩니다.type도 그대로 전달되므로 폼 안에서type="submit"이 동작합니다. render로 요소를 바꿔도 그 요소의 semantics는 유지됩니다.<a href>는role="button"으로 덮이지 않고 링크로 남습니다.- 아이콘만 있는 버튼에는
aria-label을 주세요. - focus ring은
:focus-visible에서만 나타나므로 마우스 클릭에는 보이지 않습니다. loading과readOnly는 focus를 유지합니다. tab 순서에서 사라지면 키보드 사용자가 페이지 구조를 잃기 때문입니다.- 모든 색 조합이 채움 위 글자 대비 4.5:1을 만족합니다.