ToggleButton

Action

눌러서 켜고 끄는 상태를 전환하는 토글 버튼.

Usage

눌린 상태가 유지되는 버튼(단일은 pressed, 배타 그룹은 ToggleButton.Group). 2~5개 배타 전환 자체가 목적이면 SegmentedControl

import

import

import { ToggleButton } from '@mildang/design-system/ToggleButton';

예제를 복사해 쓸 때 필요한 준비

@mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

API Reference

ToggleButton Props

Prop

Type

Default

asChild

boolean

false

children

ReactNode

지정 안 함

className

string

지정 안 함

disabled

boolean

false

endIcon

ReactNode

지정 안 함

iconColor

string

지정 안 함

outline

boolean

false

size

"xl" | "lg" | "md" | "sm" | "xs"

md

startIcon

ReactNode

지정 안 함

type

"text" | "icon_only"

text

value

string | (string & readonly string[])

지정 안 함

제어와 그룹

pressed와 onPressedChange를 사용해 단일 토글을 제어할 수 있습니다. ToggleButton.Group은 단일 또는 multiple 선택을 제공합니다.

상태와 크기

기본과 눌린 상태, 크기 5종, 비활성을 한 번에 봅니다 — 눌린 상태가 유지되는 것이 이 컴포넌트의 정체입니다.

코드

import { useState } from 'react'; import { ToggleButton } from '@mildang/design-system/ToggleButton';
export default function ToggleButtonControlledExample() { const [pressed, setPressed] = useState(false); return <ToggleButton type="text" outline pressed={pressed} onPressedChange={setPressed}>{pressed ? 'On' : 'Off'}</ToggleButton>; }

토글 그룹

단일·다중 선택 그룹을 사용합니다.

import { useState } from 'react'; import { ToggleButton } from '@mildang/design-system/ToggleButton';
export default function ToggleButtonGroupsExample() { const [single, setSingle] = useState<string[]>(['bold']); const [multiple, setMultiple] = useState<string[]>(['bold', 'italic']); const group = (value: string[], setter: (v: string[]) => void, isMultiple = false) => <ToggleButton.Group multiple={isMultiple} value={value} onValueChange={(d) => setter(d.value)}><ToggleButton value="bold" outline>Bold</ToggleButton><ToggleButton value="italic" outline>Italic</ToggleButton><ToggleButton value="underline" outline>Underline</ToggleButton></ToggleButton.Group>; return <div>{group(single, setSingle)}{group(multiple, setMultiple, true)}</div>; }

예제

Group · icon_only

import { ToggleButton } from '@mildang/design-system/ToggleButton';
import Star from '@mildang/icons/react/star-fill';

const ToggleButtonGroupIconOnlyExample = () => (
    <ToggleButton.Group multiple defaultValue={['star1']}>
      <ToggleButton value="star1" type="icon_only" outline>
        <Star />
      </ToggleButton>
      <ToggleButton value="star2" type="icon_only" outline>
        <Star />
      </ToggleButton>
      <ToggleButton value="star3" type="icon_only" outline>
        <Star />
      </ToggleButton>
    </ToggleButton.Group>
  );

export default ToggleButtonGroupIconOnlyExample;

Matrix

import { ToggleButton } from '@mildang/design-system/ToggleButton';
import { Text } from '@mildang/design-system/Text';
import Star from '@mildang/icons/react/star-fill';
import User from '@mildang/icons/react/user';
import CaretDown from '@mildang/icons/react/caret-down';
import { css } from '@mildang/styled-system/css';

/**
 * Figma `_toggle_button_base` 상태 매트릭스.
 * 열: default / hover / pressed / selected / selected_hover / selected_pressed / disabled
 * 행: text·outline / text·no-outline / icon_only·outline / icon_only·no-outline
 *
 * hover/pressed 는 `storybook-addon-pseudo-states` 로 강제 표시한다.
 * selected(_hover/_pressed) 는 ark-ui `defaultPressed` 로 시작 상태를 켜둔다.
 */
const STATE_ROWS = [
  { key: 'text-outline', label: 'Text · outline', type: 'text' as const, outline: true },
  { key: 'text-noOutline', label: 'Text · no-outline', type: 'text' as const, outline: false },
  { key: 'icon-outline', label: 'IconOnly · outline', type: 'icon_only' as const, outline: true },
  { key: 'icon-noOutline', label: 'IconOnly · no-outline', type: 'icon_only' as const, outline: false },
] as const;

type StateCol = {
  key: string;
  label: string;
  pseudo?: 'hover' | 'active';
  pressed?: boolean;
  disabled?: boolean;
};

const STATE_COLS: readonly StateCol[] = [
  { key: 'default', label: 'Default' },
  { key: 'hover', label: 'Hover', pseudo: 'hover' },
  { key: 'pressed', label: 'Pressed', pseudo: 'active' },
  { key: 'selected', label: 'Selected', pressed: true },
  { key: 'selectedHover', label: 'Selected · Hover', pressed: true, pseudo: 'hover' },
  { key: 'selectedPressed', label: 'Selected · Pressed', pressed: true, pseudo: 'active' },
  { key: 'disabled', label: 'Disabled', disabled: true },
  { key: 'selectedDisabled', label: 'Selected · Disabled', pressed: true, disabled: true },
];

const stateCellId = (rowKey: string, colKey: string) => `tb-state-${rowKey}-${colKey}`;

// gridTemplateColumns 는 Panda 정적 추출을 위해 상수 문자열 (STATE_COLS.length = 8)
const stateMatrixWrap = css({
  display: 'grid',
  gridTemplateColumns: '160px repeat(8, minmax(96px, auto))',
  gap: '8px 12px',
  alignItems: 'center',
  padding: '8px',
});

type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

type ToggleButtonMatrixProps = { size: ToggleSize };

const STORY_DEFAULT_ARGS = { size: 'md' } as ToggleButtonMatrixProps;

const ToggleButtonMatrixExampleRender = ({ size }: ToggleButtonMatrixProps) => (
    <div className={stateMatrixWrap}>
      <div />
      {STATE_COLS.map((col) => (
        <Text key={col.key} variant="caption-lg-medium" color="neutral.text.low" textAlign="center">
          {col.label}
        </Text>
      ))}

      {STATE_ROWS.flatMap((row) => [
        <Text key={`${row.key}-label`} variant="caption-lg-medium">
          {row.label}
        </Text>,
        ...STATE_COLS.map((col) => {
          const id = stateCellId(row.key, col.key);
          if (row.type === 'icon_only') {
            return (
              <ToggleButton
                key={col.key}
                id={id}
                type="icon_only"
                size={size}
                outline={row.outline}
                defaultPressed={col.pressed}
                disabled={col.disabled}
                aria-label={`${row.label} · ${col.label}`}
                justifySelf="center"
              >
                <Star />
              </ToggleButton>
            );
          }
          return (
            <ToggleButton
              key={col.key}
              id={id}
              type="text"
              size={size}
              outline={row.outline}
              defaultPressed={col.pressed}
              disabled={col.disabled}
              startIcon={<User />}
              endIcon={<CaretDown />}
            >
              Text
            </ToggleButton>
          );
        }),
      ])}
    </div>
  );

export default function ToggleButtonMatrixExample(props: Partial<ToggleButtonMatrixProps>) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ToggleButtonMatrixProps;
  return ToggleButtonMatrixExampleRender(mergedProps);
}

Size matrix

import { Text } from '@mildang/design-system/Text';
import { ToggleButton } from '@mildang/design-system/ToggleButton';
import Star from '@mildang/icons/react/star-fill';
import User from '@mildang/icons/react/user';
import CaretDown from '@mildang/icons/react/caret-down';
import { css } from '@mildang/styled-system/css';

/**
 * 사이즈별 매트릭스 (xs / sm / md / lg / xl).
 * 각 사이즈에서 `text·outline` / `text·no-outline` / `icon_only·outline` / `icon_only·no-outline` 4가지를 함께 보여준다.
 * 상태(hover/pressed 등)는 StateMatrix 에서 확인하고, 여기서는 사이즈 비교에 집중한다.
 */
const SIZE_LIST = ['xs', 'sm', 'md', 'lg', 'xl'] as const;

const SIZE_VARIANTS = [
  { key: 'text-outline', label: 'Text · outline', type: 'text' as const, outline: true },
  { key: 'text-noOutline', label: 'Text · no-outline', type: 'text' as const, outline: false },
  { key: 'icon-outline', label: 'IconOnly · outline', type: 'icon_only' as const, outline: true },
  { key: 'icon-noOutline', label: 'IconOnly · no-outline', type: 'icon_only' as const, outline: false },
] as const;

// gridTemplateColumns 는 Panda 정적 추출을 위해 상수 문자열 (SIZE_VARIANTS.length = 4).
// max-content 로 각 컬럼이 컨텐츠(xl 크기 버튼) 폭에 hug 되게 하여 불필요한 여백을 없앤다.
const sizeMatrixWrap = css({
  display: 'grid',
  gridTemplateColumns: '48px repeat(4, max-content)',
  gap: '8px 24px',
  alignItems: 'center',
  justifyItems: 'center',
  padding: '8px',
});

const ToggleButtonSizeMatrixExample = () => (
    <div className={sizeMatrixWrap}>
      <div />
      {SIZE_VARIANTS.map((v) => (
        <Text key={v.key} variant="caption-lg-medium" color="neutral.text.low" textAlign="center">
          {v.label}
        </Text>
      ))}

      {SIZE_LIST.flatMap((size) => [
        <Text key={`${size}-label`} variant="caption-lg-medium" justifySelf="start">
          {size}
        </Text>,
        ...SIZE_VARIANTS.map((v) =>
          v.type === 'icon_only' ? (
            <ToggleButton
              key={`${size}-${v.key}`}
              type="icon_only"
              size={size}
              outline={v.outline}
              aria-label={`${size} · ${v.label}`}
            >
              <Star />
            </ToggleButton>
          ) : (
            <ToggleButton
              key={`${size}-${v.key}`}
              type="text"
              size={size}
              outline={v.outline}
              startIcon={<User />}
              endIcon={<CaretDown />}
            >
              Text
            </ToggleButton>
          ),
        ),
      ])}
    </div>
  );

export default ToggleButtonSizeMatrixExample;

Default

import type { ComponentProps } from 'react';
import { ToggleButton } from '@mildang/design-system/ToggleButton';

const STORY_DEFAULT_ARGS = { ...({}), ...({ type: 'text', children: 'Label' }) } as ComponentProps<typeof ToggleButton>;

export default function ToggleButtonDefaultExample(props: Partial<ComponentProps<typeof ToggleButton>> = {}) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ToggleButton>;
  return <ToggleButton {...mergedProps} />;
}

Pressed

import type { ComponentProps } from 'react';
import { ToggleButton } from '@mildang/design-system/ToggleButton';

const STORY_DEFAULT_ARGS = { ...({}), ...({ type: 'text', defaultPressed: true, children: 'Label' }) } as ComponentProps<typeof ToggleButton>;

export default function ToggleButtonPressedExample(props: Partial<ComponentProps<typeof ToggleButton>> = {}) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ToggleButton>;
  return <ToggleButton {...mergedProps} />;
}

Outline

import type { ComponentProps } from 'react';
import { ToggleButton } from '@mildang/design-system/ToggleButton';

const STORY_DEFAULT_ARGS = { ...({}), ...({ type: 'text', outline: true, children: 'Label' }) } as ComponentProps<typeof ToggleButton>;

export default function ToggleButtonOutlineExample(props: Partial<ComponentProps<typeof ToggleButton>> = {}) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ToggleButton>;
  return <ToggleButton {...mergedProps} />;
}

IconOnly

import type { ComponentProps } from 'react';
import Star from '@mildang/icons/react/star-fill';
import { ToggleButton } from '@mildang/design-system/ToggleButton';

const STORY_DEFAULT_ARGS = { ...({}), ...({ type: 'icon_only', children: <Star /> }) } as ComponentProps<typeof ToggleButton>;

export default function ToggleButtonIconOnlyExample(props: Partial<ComponentProps<typeof ToggleButton>> = {}) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ToggleButton>;
  return <ToggleButton {...mergedProps} />;
}

text · startIcon + endIcon

import type { ComponentProps } from 'react';
import User from '@mildang/icons/react/user';
import ChevronDown from '@mildang/icons/react/chevron-down';
import { ToggleButton } from '@mildang/design-system/ToggleButton';

const STORY_DEFAULT_ARGS = { ...({}), ...({
    type: 'text',
    outline: true,
    startIcon: <User />,
    endIcon: <ChevronDown />,
    children: 'Label',
  }) } as ComponentProps<typeof ToggleButton>;

export default function ToggleButtonWithStartEndIconExample(props: Partial<ComponentProps<typeof ToggleButton>> = {}) {
  const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ToggleButton>;
  return <ToggleButton {...mergedProps} />;
}