본문으로 건너뛰기
Amineslab UI

Component

FormActions

기본 취소·제출 버튼 또는 커스텀 액션을 PC·모바일에 맞게 배치합니다.

Playground

옵션을 조절하며 화면과 사용 코드를 함께 확인하세요.

PC 1280px
Controls

actions: 기본 버튼 / 커스텀 목록 / 빈 목록

cancelOptions.show

cancelOptions.children

cancelOptions.variant

cancelOptions.color

submitOptions.show

submitOptions.children

submitOptions.variant

submitOptions.color

두 버튼의 size

submitOptions.loading

cancelOptions.disabled

submitOptions.disabled

destructiveOptions 지정 여부

hintOptions.show

hintShow를 켜면 안내 문구에 적용됩니다.

hintShow를 켜면 안내의 ReactNode 표현에 적용됩니다.

Code
import { useState } from 'react'

import { type FormActionsProps, Button, TextField, FormActions } from '@amineslab/ui'

const formActionsInitial = {
    cancelShow: true,
    submitShow: true,
    cancelText: '취소',
    submitText: '저장',
    cancelVariant: 'ghost',
    submitVariant: 'primary',
    cancelColor: 'inverse',
    submitColor: 'inverse',
    size: 'md',
    loading: false,
    cancelDisabled: false,
    submitDisabled: false,
    hintShow: false,
    hintText: '변경 사항을 확인한 뒤 저장해 주세요.',
    richHint: false,
    actionsMode: 'builtin',
    destructiveShow: false,
};

function FormActionsExample(options: Partial<typeof formActionsInitial>) {
    const state = { ...formActionsInitial, ...options };
    const [saved, setSaved] = useState(false);
    const [name, setName] = useState('Amineslab UI');
    const reset = () => {
        setName('Amineslab UI');
        setSaved(false);
    };
    const actions: FormActionsProps['actions'] = state.actionsMode === 'empty'
        ? []
        : state.actionsMode === 'custom'
            ? [
                { children: '임시 저장', variant: 'ghost', onClick: () => setSaved(true) },
                <Button key="preview" type="button" variant="outline" onClick={() => setName('미리보기 프로젝트')}>
              미리보기
            </Button>,
                { children: saved ? '저장됨' : '게시', type: 'submit', color: 'brand' },
            ]
            : undefined;
    return (<form className="grid w-full gap-6" onSubmit={(event) => {
            event.preventDefault();
            setSaved(true);
        }}>
      <TextField label="프로젝트 이름" value={name} onValueChange={(value) => {
            setName(value);
            setSaved(false);
        }}/>
      <FormActions cancelOptions={{
            show: state.cancelShow,
            children: state.cancelText,
            onClick: reset,
            disabled: state.cancelDisabled,
            variant: state.cancelVariant as 'ghost' | 'outline' | 'secondary',
            color: state.cancelColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} destructiveOptions={state.destructiveShow ? { children: '삭제', onClick: reset } : undefined} submitOptions={{
            show: state.submitShow,
            children: saved ? '저장됨' : state.submitText,
            disabled: state.submitDisabled,
            loading: state.loading,
            loadingLabel: '저장 중…',
            variant: state.submitVariant as 'primary' | 'outline' | 'secondary',
            color: state.submitColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} actions={actions} hintOptions={{
            show: state.hintShow,
            children: state.richHint ? (<span>
              <strong>안내: </strong>
              {state.hintText}
            </span>) : (state.hintText),
        }}/>
    </form>);
}

export default function ExamplePreview() {
  return (<FormActionsExample {...({"cancelShow":true,"submitShow":true,"cancelText":"취소","submitText":"저장","cancelVariant":"ghost","submitVariant":"primary","cancelColor":"inverse","submitColor":"inverse","size":"md","loading":false,"cancelDisabled":false,"submitDisabled":false,"hintShow":false,"hintText":"변경 사항을 확인한 뒤 저장해 주세요.","richHint":false,"actionsMode":"builtin","destructiveShow":false} as const)}/>)
}

Props

@amineslab/ui에서 FormActions를 가져옵니다.

import { FormActions } from '@amineslab/ui'

FormActions

버튼 텍스트는 children을 사용하며 모든 ButtonProps를 전달할 수 있습니다.

NameTypeDefaultDescription
cancelOptionsActionButtonProps-기본값: show true, type button, variant ghost, color inverse, size lg, children 취소. onClick으로 취소 동작을 지정합니다.
submitOptionsActionButtonProps-기본값: show true, type submit, variant primary, color inverse, size lg, children 저장. loading·loadingLabel·disabled·form·아이콘 등을 ButtonProps로 설정합니다.
actions(ActionButtonProps | ReactNode)[]-지정하면 기본 버튼 대신 이 목록만 표시합니다. []는 버튼 없음입니다. 옵션 버튼은 type button·size md이 기본이며 show false 항목은 숨깁니다.
destructiveOptionsActionButtonProps-기본값: type button, variant outline, color danger, size lg. actions와 별개로 버튼 그룹 밖에 렌더합니다.
hintOptions{ show?: boolean; children?: ReactNode; id?: string }-show 기본값은 true입니다. 문자열·JSX를 표시하며 false 또는 빈 콘텐츠는 영역과 ARIA 연결을 제거합니다. 기본 제출 버튼과 옵션형 actions에 연결하고 직접 전달한 ReactNode의 ARIA 속성은 변경하지 않습니다.
classNamestring-외곽 배치를 조정합니다.

ActionButtonProps

ButtonProps & { show?: boolean }. 버튼별 설정은 독립적입니다.

NameTypeDefaultDescription
showbooleantruefalse면 렌더링하지 않습니다.
childrenReactNode-버튼 텍스트 또는 JSX입니다.
loading / loadingLabel / disabledButtonProps-제출 로딩이 취소를 자동으로 비활성화하지 않습니다. 필요하면 cancelOptions.disabled도 명시합니다.

Examples

자주 쓰는 조합을 살펴보고, 필요한 예시의 코드를 펼쳐 확인하세요.

힌트와 저장 중 상태

힌트는 데스크톱에서 버튼을 밀어내고 모바일에서는 액션 위에 쌓입니다. pending 중에는 중복 실행을 막습니다.

변경 사항을 확인한 뒤 저장해 주세요.
import { useState } from 'react'

import { type FormActionsProps, Button, TextField, FormActions } from '@amineslab/ui'

const formActionsInitial = {
    cancelShow: true,
    submitShow: true,
    cancelText: '취소',
    submitText: '저장',
    cancelVariant: 'ghost',
    submitVariant: 'primary',
    cancelColor: 'inverse',
    submitColor: 'inverse',
    size: 'md',
    loading: false,
    cancelDisabled: false,
    submitDisabled: false,
    hintShow: false,
    hintText: '변경 사항을 확인한 뒤 저장해 주세요.',
    richHint: false,
    actionsMode: 'builtin',
    destructiveShow: false,
};

function FormActionsExample(options: Partial<typeof formActionsInitial>) {
    const state = { ...formActionsInitial, ...options };
    const [saved, setSaved] = useState(false);
    const [name, setName] = useState('Amineslab UI');
    const reset = () => {
        setName('Amineslab UI');
        setSaved(false);
    };
    const actions: FormActionsProps['actions'] = state.actionsMode === 'empty'
        ? []
        : state.actionsMode === 'custom'
            ? [
                { children: '임시 저장', variant: 'ghost', onClick: () => setSaved(true) },
                <Button key="preview" type="button" variant="outline" onClick={() => setName('미리보기 프로젝트')}>
              미리보기
            </Button>,
                { children: saved ? '저장됨' : '게시', type: 'submit', color: 'brand' },
            ]
            : undefined;
    return (<form className="grid w-full gap-6" onSubmit={(event) => {
            event.preventDefault();
            setSaved(true);
        }}>
      <TextField label="프로젝트 이름" value={name} onValueChange={(value) => {
            setName(value);
            setSaved(false);
        }}/>
      <FormActions cancelOptions={{
            show: state.cancelShow,
            children: state.cancelText,
            onClick: reset,
            disabled: state.cancelDisabled,
            variant: state.cancelVariant as 'ghost' | 'outline' | 'secondary',
            color: state.cancelColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} destructiveOptions={state.destructiveShow ? { children: '삭제', onClick: reset } : undefined} submitOptions={{
            show: state.submitShow,
            children: saved ? '저장됨' : state.submitText,
            disabled: state.submitDisabled,
            loading: state.loading,
            loadingLabel: '저장 중…',
            variant: state.submitVariant as 'primary' | 'outline' | 'secondary',
            color: state.submitColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} actions={actions} hintOptions={{
            show: state.hintShow,
            children: state.richHint ? (<span>
              <strong>안내: </strong>
              {state.hintText}
            </span>) : (state.hintText),
        }}/>
    </form>);
}

export default function ExamplePreview() {
  return (<FormActionsExample hintShow loading cancelDisabled/>)
}

옵션과 커스텀 버튼 혼합

actions를 지정하면 기본 취소·제출 옵션은 무시합니다. 빈 배열은 버튼을 숨깁니다.

안내: 변경 사항을 확인한 뒤 저장해 주세요.
import { useState } from 'react'

import { type FormActionsProps, Button, TextField, FormActions } from '@amineslab/ui'

const formActionsInitial = {
    cancelShow: true,
    submitShow: true,
    cancelText: '취소',
    submitText: '저장',
    cancelVariant: 'ghost',
    submitVariant: 'primary',
    cancelColor: 'inverse',
    submitColor: 'inverse',
    size: 'md',
    loading: false,
    cancelDisabled: false,
    submitDisabled: false,
    hintShow: false,
    hintText: '변경 사항을 확인한 뒤 저장해 주세요.',
    richHint: false,
    actionsMode: 'builtin',
    destructiveShow: false,
};

function FormActionsExample(options: Partial<typeof formActionsInitial>) {
    const state = { ...formActionsInitial, ...options };
    const [saved, setSaved] = useState(false);
    const [name, setName] = useState('Amineslab UI');
    const reset = () => {
        setName('Amineslab UI');
        setSaved(false);
    };
    const actions: FormActionsProps['actions'] = state.actionsMode === 'empty'
        ? []
        : state.actionsMode === 'custom'
            ? [
                { children: '임시 저장', variant: 'ghost', onClick: () => setSaved(true) },
                <Button key="preview" type="button" variant="outline" onClick={() => setName('미리보기 프로젝트')}>
              미리보기
            </Button>,
                { children: saved ? '저장됨' : '게시', type: 'submit', color: 'brand' },
            ]
            : undefined;
    return (<form className="grid w-full gap-6" onSubmit={(event) => {
            event.preventDefault();
            setSaved(true);
        }}>
      <TextField label="프로젝트 이름" value={name} onValueChange={(value) => {
            setName(value);
            setSaved(false);
        }}/>
      <FormActions cancelOptions={{
            show: state.cancelShow,
            children: state.cancelText,
            onClick: reset,
            disabled: state.cancelDisabled,
            variant: state.cancelVariant as 'ghost' | 'outline' | 'secondary',
            color: state.cancelColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} destructiveOptions={state.destructiveShow ? { children: '삭제', onClick: reset } : undefined} submitOptions={{
            show: state.submitShow,
            children: saved ? '저장됨' : state.submitText,
            disabled: state.submitDisabled,
            loading: state.loading,
            loadingLabel: '저장 중…',
            variant: state.submitVariant as 'primary' | 'outline' | 'secondary',
            color: state.submitColor as 'inverse' | 'brand' | 'danger',
            size: state.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl',
        }} actions={actions} hintOptions={{
            show: state.hintShow,
            children: state.richHint ? (<span>
              <strong>안내: </strong>
              {state.hintText}
            </span>) : (state.hintText),
        }}/>
    </form>);
}

export default function ExamplePreview() {
  return (<FormActionsExample actionsMode="custom" hintShow richHint/>)
}

사용 기준

권장하는 사용
  • PC에서는 안내를 왼쪽, 버튼을 오른쪽에 배치하고 세로 중앙을 맞춥니다.
  • 모바일에서는 안내를 버튼 위에 가운데 정렬합니다. 버튼은 같은 폭으로 배치하고, 공간이 부족하면 세로로 쌓습니다.
  • 저장은 type="submit"이며 부모 form의 onSubmit으로 처리합니다.
  • 취소는 type="button"입니다. cancelOptions.onClick에 취소 동작을 지정하세요.
  • actions를 지정하면 cancelOptions와 submitOptions는 무시합니다. 빈 배열을 지정하면 버튼을 표시하지 않습니다.
  • 커스텀 ReactNode의 스타일과 이벤트는 그대로 유지합니다.
  • destructiveOptions는 삭제처럼 되돌리기 어려운 작업입니다. PC에서는 행의 시작 쪽, 모바일에서는 맨 위에 두어 주 작업과 떨어뜨립니다.
  • 링크 이동 액션은 actions에 Button asChild로 감싼 링크 노드를 넣습니다.
피해야 할 사용
  • 겉모양만으로 사용을 결정하지 마세요.
  • 의미와 키보드 동작, 모바일에서의 흐름을 함께 확인하세요.

접근성

화면에 맞는 이름을 제공하고, 키보드만으로 작업을 마칠 수 있는지 확인하세요.

  • Enter: submit 버튼이 속한 폼을 제출합니다.