IconButton
Action
텍스트 없이 아이콘으로 하나의 행동을 실행하는 버튼.
Usage
import
API Reference
사용 가이드
예제
Usage
공간이 좁고 아이콘의 의미가 명확한 도구 모음 닫기·검색·삭제처럼 익숙한 단일 행동
import
import
import { IconButton } from '@mildang/design-system/IconButton';예제를 복사해 쓸 때 필요한 준비
• @mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
API Reference
IconButton Props
Prop
Type
Default
asChild
boolean
false
className
string
지정 안 함
disabled
boolean
false
loading
boolean
false
size
IconButtonSize
md
variant
"impact" | "primary" | "secondary" | "tertiary"
tertiary
사용 가이드
권장
- 모든 아이콘 버튼에 동작을 설명하는 aria-label을 제공한다.
- 행동의 우선순위에 맞는 variant를 사용한다.
지양
- 의미가 모호한 아이콘을 설명 없이 단독으로 사용하지 않는다.
- 여러 행동을 하나의 아이콘 버튼에 결합하지 않는다.
예제
Demo
아이콘 버튼 args와 Star 아이콘을 함께 전달합니다.
import Star from '@mildang/icons/react/star-fill';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
export default function IconButtonDemoExample(args: IconButtonProps = {}) {
return (
<IconButton
{...{
variant: 'primary' as const,
size: 'lg' as const,
loading: false,
'aria-label': '즐겨찾기',
...args,
}}
>
<Star />
</IconButton>
);
}
위계별 상태
비활성·로딩 등 아이콘 버튼 상태를 비교합니다.
import Star from '@mildang/icons/react/star-fill';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';
type StateConfig = { label: string; id?: string; props?: Partial<IconButtonProps> };
const createStates = (prefix: string): StateConfig[] => [
{ label: 'Inactive', id: `${prefix}-inactive`, props: {} },
{ label: 'Hover', id: `${prefix}-hover`, props: {} },
{ label: 'Pressed', id: `${prefix}-active`, props: {} },
{ label: 'Disabled', id: `${prefix}-disabled`, props: { disabled: true } },
{ label: 'Loading', id: `${prefix}-loading`, props: { loading: true } },
];
export default function IconButtonStateGridExample({
states,
args,
}: {
states?: StateConfig[];
args?: IconButtonProps;
}) {
const resolvedStates = states ?? createStates('primary');
const resolvedArgs = {
variant: 'primary' as const,
size: 'lg' as const,
'aria-label': '즐겨찾기',
...(args ?? {}),
};
return (
<div className={css({ display: 'flex', gap: '16px' })}>
{resolvedStates.map((state) => (
<div
className={css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' })}
key={state.label}
>
<Text variant="title-lg-medium" color="neutral.text.low">
{state.label}
</Text>
<IconButton id={state.id} {...resolvedArgs} {...state.props}>
<Star />
</IconButton>
</div>
))}
</div>
);
}
export function createIconButtonStates(prefix: string) {
return createStates(prefix);
}
위계 비교
네 단계 위계를 한 화면에서 비교합니다.
import Star from '@mildang/icons/react/star-fill';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';
const variants = ['impact', 'primary', 'secondary', 'tertiary'] as const;
const gridStyle = css({ display: 'flex', gap: '16px' });
const itemStyle = css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' });
export default function IconButtonHierarchyExample(args: IconButtonProps = {}) {
const resolvedArgs = {
variant: 'primary' as const,
size: 'lg' as const,
'aria-label': '즐겨찾기',
...args,
};
return (
<div className={gridStyle}>
{variants.map((variant) => (
<div className={itemStyle} key={variant}>
<Text variant="title-lg-medium" color="neutral.text.low">
{variant}
</Text>
<IconButton {...resolvedArgs} variant={variant}>
<Star />
</IconButton>
</div>
))}
</div>
);
}
Destructive
위계별 위험 액션 톤을 비교합니다.
import DeleteIcon from '@mildang/icons/react/delete';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';
const variants = ['primary', 'secondary', 'tertiary'] as const;
const gridStyle = css({ display: 'flex', gap: '16px' });
const itemStyle = css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' });
export default function IconButtonDestructiveExample(args: IconButtonProps = {}) {
const resolvedArgs = { variant: 'primary' as const, size: 'lg' as const, 'aria-label': '삭제', ...args };
return (
<div className={gridStyle}>
{variants.map((variant) => (
<div className={itemStyle} key={variant}>
<Text variant="title-lg-medium" color="neutral.text.low">
{variant}
</Text>
<IconButton
{...resolvedArgs}
variant={variant}
{...(variant === 'primary' ? { bg: 'critical.fill.base' } : { color: 'critical.fill.base' })}
>
<DeleteIcon />
</IconButton>
</div>
))}
</div>
);
}
Success
위계별 긍정 액션 톤을 비교합니다.
import Star from '@mildang/icons/react/star-fill';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
import { Text } from '@mildang/design-system/Text';
import { css } from '@mildang/styled-system/css';
const variants = ['primary', 'secondary', 'tertiary'] as const;
const gridStyle = css({ display: 'flex', gap: '16px' });
const itemStyle = css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' });
export default function IconButtonSuccessExample(args: IconButtonProps = {}) {
const resolvedArgs = { variant: 'primary' as const, size: 'lg' as const, 'aria-label': '완료', ...args };
return (
<div className={gridStyle}>
{variants.map((variant) => (
<div className={itemStyle} key={variant}>
<Text variant="title-lg-medium" color="neutral.text.low">
{variant}
</Text>
<IconButton
{...resolvedArgs}
variant={variant}
{...(variant === 'primary' ? { bg: 'positive.fill.base' } : { color: 'positive.fill.base' })}
>
<Star />
</IconButton>
</div>
))}
</div>
);
}
Migration Guide
구버전 위계와 신규 위계의 대응을 확인합니다.
import Star from '@mildang/icons/react/star-fill';
import DeleteIcon from '@mildang/icons/react/delete';
import { IconButton } from '@mildang/design-system/IconButton';
import { VStack } from '@mildang/styled-system/jsx';
import { css } from '@mildang/styled-system/css';
import type { ReactNode } from 'react';
type MigrationRow = { scenario: string; before: string; after: string; preview: ReactNode };
const tableStyle = css({
display: 'grid',
gridTemplateColumns: 'auto 1fr 1fr auto',
width: '100%',
maxWidth: '1100px',
});
const cellStyle = css({
px: '3',
py: '3',
borderBottom: '1px solid token(colors.neutral.border.low)',
display: 'flex',
alignItems: 'center',
});
const codeStyle = css({ fontFamily: 'mono', fontSize: 'sm', whiteSpace: 'pre', overflow: 'auto' });
const headingStyle = css({
px: '3',
py: '2',
borderBottom: '2px solid token(colors.neutral.border.base)',
fontWeight: 'bold',
background: 'neutral.surface.high',
});
const titleStyle = css({ fontSize: 'lg', fontWeight: 'bold', mt: '6', mb: '2' });
const MigrationTable = ({ title, rows }: { title: string; rows: MigrationRow[] }) => (
<section>
<div className={titleStyle}>{title}</div>
<div className={tableStyle}>
{['케이스', 'Before (구버전)', 'After (신버전)', '결과'].map((label) => (
<div className={headingStyle} key={label}>
{label}
</div>
))}
{rows.map((row) => (
<div className={css({ display: 'contents' })} key={row.scenario}>
<div className={cellStyle}>{row.scenario}</div>
<div className={cellStyle}>
<code className={codeStyle}>{row.before}</code>
</div>
<div className={cellStyle}>
<code className={codeStyle}>{row.after}</code>
</div>
<div className={cellStyle}>{row.preview}</div>
</div>
))}
</div>
</section>
);
const migrationRows: { title: string; rows: MigrationRow[] }[] = [
{
title: '기본 위계 (color 없음)',
rows: [
{
scenario: 'Contained',
before: '<IconButton variant="contained">Star</IconButton>',
after: '<IconButton variant="primary">Star</IconButton>',
preview: (
<IconButton variant="primary" aria-label="Contained">
<Star />
</IconButton>
),
},
{
scenario: 'Outlined',
before: '<IconButton variant="outlined">Star</IconButton>',
after: '<IconButton variant="secondary">Star</IconButton>',
preview: (
<IconButton variant="secondary" aria-label="Outlined">
<Star />
</IconButton>
),
},
{
scenario: 'Text / Ghost / None',
before: '<IconButton variant="text">Star</IconButton>',
after: '<IconButton variant="tertiary">Star</IconButton>',
preview: (
<IconButton variant="tertiary" aria-label="Text">
<Star />
</IconButton>
),
},
],
},
{
title: 'Destructive (color="error")',
rows: [
{
scenario: 'Primary + error',
before: '<IconButton variant="contained" color="error">Delete</IconButton>',
after: '<IconButton variant="primary" bg="critical.fill.base">Delete</IconButton>',
preview: (
<IconButton variant="primary" bg="critical.fill.base" aria-label="Error">
<DeleteIcon />
</IconButton>
),
},
{
scenario: 'Secondary + error',
before: '<IconButton variant="outlined" color="error">Delete</IconButton>',
after: '<IconButton variant="secondary" color="critical.fill.base">Delete</IconButton>',
preview: (
<IconButton variant="secondary" color="critical.fill.base" aria-label="Error outline">
<DeleteIcon />
</IconButton>
),
},
{
scenario: 'Tertiary + error',
before: '<IconButton variant="text" color="error">Delete</IconButton>',
after: '<IconButton variant="tertiary" color="critical.fill.base">Delete</IconButton>',
preview: (
<IconButton variant="tertiary" color="critical.fill.base" aria-label="Error text">
<DeleteIcon />
</IconButton>
),
},
],
},
{
title: 'Success (color="success")',
rows: [
{
scenario: 'Primary + success',
before: '<IconButton variant="contained" color="success">Star</IconButton>',
after: '<IconButton variant="primary" bg="positive.fill.base">Star</IconButton>',
preview: (
<IconButton variant="primary" bg="positive.fill.base" aria-label="Success">
<Star />
</IconButton>
),
},
{
scenario: 'Secondary + success',
before: '<IconButton variant="outlined" color="success">Star</IconButton>',
after: '<IconButton variant="secondary" color="positive.fill.base">Star</IconButton>',
preview: (
<IconButton variant="secondary" color="positive.fill.base" aria-label="Success outline">
<Star />
</IconButton>
),
},
{
scenario: 'Tertiary + success',
before: '<IconButton variant="text" color="success">Star</IconButton>',
after: '<IconButton variant="tertiary" color="positive.fill.base">Star</IconButton>',
preview: (
<IconButton variant="tertiary" color="positive.fill.base" aria-label="Success text">
<Star />
</IconButton>
),
},
],
},
{
title: '기타 color',
rows: [
{
scenario: 'color="impact" → variant="impact"',
before: '<IconButton variant="contained" color="impact">Star</IconButton>',
after: '<IconButton variant="impact">Star</IconButton>',
preview: (
<IconButton variant="impact" aria-label="Impact">
<Star />
</IconButton>
),
},
{
scenario: 'color="gray" (Dialog close 등)',
before: '<IconButton color="gray">Close</IconButton>',
after: '<IconButton variant="tertiary">Close</IconButton>',
preview: (
<IconButton variant="tertiary" aria-label="Close">
<Star />
</IconButton>
),
},
{
scenario: '커스텀 토큰',
before: '<IconButton color={adaptive.red500}>Star</IconButton>',
after: '<IconButton color="critical.fill.base">Star</IconButton>',
preview: (
<IconButton variant="tertiary" color="critical.fill.base" aria-label="Custom token">
<Star />
</IconButton>
),
},
],
},
];
export default function IconButtonMigrationGuideExample() {
return (
<VStack alignItems="flex-start" gap="8">
{migrationRows.map((table) => (
<MigrationTable key={table.title} {...table} />
))}
</VStack>
);
/* return (
<VStack alignItems="flex-start" gap="16">
<HStack gap="8" flexWrap="wrap">
<IconButton variant="primary" aria-label="Contained">
<Star />
</IconButton>
<IconButton variant="secondary" aria-label="Outlined">
<Star />
</IconButton>
<IconButton variant="tertiary" aria-label="Text">
<Star />
</IconButton>
</HStack>
<HStack gap="8" flexWrap="wrap">
<IconButton variant="primary" bg="critical.fill.base" aria-label="Error">
<DeleteIcon />
</IconButton>
<IconButton variant="secondary" color="critical.fill.base" aria-label="Error outline">
<DeleteIcon />
</IconButton>
<IconButton variant="tertiary" color="critical.fill.base" aria-label="Error text">
<DeleteIcon />
</IconButton>
</HStack>
<HStack gap="8" flexWrap="wrap">
<IconButton variant="primary" bg="positive.fill.base" aria-label="Success">
<Star />
</IconButton>
<IconButton variant="secondary" color="positive.fill.base" aria-label="Success outline">
<Star />
</IconButton>
<IconButton variant="tertiary" color="positive.fill.base" aria-label="Success text">
<Star />
</IconButton>
</HStack>
<HStack gap="8" flexWrap="wrap">
<IconButton variant="impact" aria-label="Impact">
<Star />
</IconButton>
<IconButton variant="tertiary" aria-label="color gray">
<Star />
</IconButton>
<IconButton variant="tertiary" color="critical.fill.base" aria-label="custom token">
<Star />
</IconButton>
</HStack>
</VStack>
); */
}
Ref 사용
asChild로 실제 button ref를 연결하는 예제입니다.
import { useRef } from 'react';
import Star from '@mildang/icons/react/star-fill';
import { IconButton, type IconButtonProps } from '@mildang/design-system/IconButton';
export default function IconButtonWithRefExample(args: IconButtonProps = {}) {
const ref = useRef<HTMLButtonElement>(null);
const resolvedArgs = {
variant: 'primary' as const,
size: 'md' as const,
'aria-label': '즐겨찾기',
...args,
};
return (
<IconButton {...resolvedArgs} asChild>
<button ref={ref} onClick={() => console.log(ref.current)}>
<Star />
</button>
</IconButton>
);
}