Skip to Content

Stack

요소들을 수직 또는 수평으로 배치하는 레이아웃 컴포넌트입니다.

개요

Stack은 요소들을 일정한 간격으로 수직(VStack) 또는 수평(HStack)으로 배치하는 컴포넌트입니다. Cals 예약 관리 시스템에서 예약 정보 리스트, 폼 레이아웃, 버튼 그룹 등에 사용됩니다.

주요 특징:

  • 수직/수평 배치 선택
  • 간격(gap) 자동 관리
  • 정렬 옵션 (start, center, end, stretch)
  • 간단한 API

설치

npx @vortex/cli add stack

기본 사용법

import { VStack, HStack } from "@vortex/ui"; export default function ReservationInfo() { return ( <VStack spacing={4}> <div>고객명: 홍길동</div> <div>서비스: 헤어 커트</div> <div>시간: 2024-01-15 14:00</div> </VStack> ); }

Variants

VStack (수직 스택)

{ /* 기본 수직 스택 */ } <VStack spacing={4}> <div className="p-4 bg-gray-100 rounded">항목 1</div> <div className="p-4 bg-gray-100 rounded">항목 2</div> <div className="p-4 bg-gray-100 rounded">항목 3</div> </VStack>; { /* 왼쪽 정렬 */ } <VStack spacing={4} align="start"> <div className="p-4 bg-gray-100 rounded">왼쪽</div> <div className="p-4 bg-gray-100 rounded w-32">정렬</div> </VStack>; { /* 중앙 정렬 */ } <VStack spacing={4} align="center"> <div className="p-4 bg-gray-100 rounded">중앙</div> <div className="p-4 bg-gray-100 rounded w-32">정렬</div> </VStack>; { /* 오른쪽 정렬 */ } <VStack spacing={4} align="end"> <div className="p-4 bg-gray-100 rounded">오른쪽</div> <div className="p-4 bg-gray-100 rounded w-32">정렬</div> </VStack>; { /* 전체 너비 */ } <VStack spacing={4} align="stretch"> <div className="p-4 bg-gray-100 rounded">전체</div> <div className="p-4 bg-gray-100 rounded">너비</div> </VStack>;

HStack (수평 스택)

{ /* 기본 수평 스택 */ } <HStack spacing={4}> <div className="p-4 bg-gray-100 rounded">항목 1</div> <div className="p-4 bg-gray-100 rounded">항목 2</div> <div className="p-4 bg-gray-100 rounded">항목 3</div> </HStack>; { /* 위쪽 정렬 */ } <HStack spacing={4} align="start"> <div className="p-4 bg-gray-100 rounded h-16">위쪽</div> <div className="p-4 bg-gray-100 rounded h-24">정렬</div> </HStack>; { /* 중앙 정렬 */ } <HStack spacing={4} align="center"> <div className="p-4 bg-gray-100 rounded h-16">중앙</div> <div className="p-4 bg-gray-100 rounded h-24">정렬</div> </HStack>; { /* 아래쪽 정렬 */ } <HStack spacing={4} align="end"> <div className="p-4 bg-gray-100 rounded h-16">아래쪽</div> <div className="p-4 bg-gray-100 rounded h-24">정렬</div> </HStack>; { /* 전체 높이 */ } <HStack spacing={4} align="stretch"> <div className="p-4 bg-gray-100 rounded">전체</div> <div className="p-4 bg-gray-100 rounded">높이</div> </HStack>;

간격(Spacing) 설정

{ /* 간격 없음 */ } <VStack spacing={0}> <div className="p-4 bg-gray-100">간격 0</div> <div className="p-4 bg-gray-100">간격 0</div> </VStack>; { /* 작은 간격 */ } <VStack spacing={2}> <div className="p-4 bg-gray-100 rounded">간격 2</div> <div className="p-4 bg-gray-100 rounded">간격 2</div> </VStack>; { /* 중간 간격 (기본값) */ } <VStack spacing={4}> <div className="p-4 bg-gray-100 rounded">간격 4</div> <div className="p-4 bg-gray-100 rounded">간격 4</div> </VStack>; { /* 큰 간격 */ } <VStack spacing={8}> <div className="p-4 bg-gray-100 rounded">간격 8</div> <div className="p-4 bg-gray-100 rounded">간격 8</div> </VStack>;

분산 정렬 (Justify)

{ /* 시작점 정렬 */ } <HStack spacing={4} justify="start"> <div className="p-4 bg-gray-100 rounded">시작</div> <div className="p-4 bg-gray-100 rounded">정렬</div> </HStack>; { /* 중앙 정렬 */ } <HStack spacing={4} justify="center"> <div className="p-4 bg-gray-100 rounded">중앙</div> <div className="p-4 bg-gray-100 rounded">정렬</div> </HStack>; { /* 끝점 정렬 */ } <HStack spacing={4} justify="end"> <div className="p-4 bg-gray-100 rounded">끝점</div> <div className="p-4 bg-gray-100 rounded">정렬</div> </HStack>; { /* 균등 분산 */ } <HStack spacing={4} justify="between"> <div className="p-4 bg-gray-100 rounded">균등</div> <div className="p-4 bg-gray-100 rounded">분산</div> <div className="p-4 bg-gray-100 rounded">정렬</div> </HStack>; { /* 전체 균등 분산 */ } <HStack spacing={4} justify="around"> <div className="p-4 bg-gray-100 rounded">전체</div> <div className="p-4 bg-gray-100 rounded">균등</div> <div className="p-4 bg-gray-100 rounded">분산</div> </HStack>;

Cals 브랜딩

예약 정보 스택 (상태별)

import { VStack } from "@vortex/ui"; { /* Confirmed - 예약 확정 */ } <VStack spacing={3} className="p-6 border-l-4 border-[#03a9f4] bg-[#03a9f4]/5 rounded-r-lg" > <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">고객</span> <span className="font-medium">홍길동</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">서비스</span> <span className="font-medium">헤어 커트</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">시간</span> <span className="font-medium text-[#03a9f4]">2024-01-15 14:00</span> </div> </VStack>; { /* Pending - 예약 대기 */ } <VStack spacing={3} className="p-6 border-l-4 border-[#ff9800] bg-[#ff9800]/5 rounded-r-lg" > <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">고객</span> <span className="font-medium">김고객</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">서비스</span> <span className="font-medium">헤어 펌</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">시간</span> <span className="font-medium text-[#ff9800]">2024-01-15 15:00</span> </div> </VStack>; { /* Completed - 서비스 완료 */ } <VStack spacing={3} className="p-6 border-l-4 border-[#9c27b0] bg-[#9c27b0]/5 rounded-r-lg" > <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">고객</span> <span className="font-medium">박고객</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">서비스</span> <span className="font-medium">헤어 염색</span> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">완료 시간</span> <span className="font-medium text-[#9c27b0]">2024-01-14 16:00</span> </div> </VStack>;

Primary Pink 액션 버튼 그룹

import { HStack } from '@vortex/ui' <HStack spacing={4} justify="end"> <button className="px-6 py-2 rounded-md border border-gray-300 hover:bg-gray-100"> 취소 </button> <button className="px-6 py-2 rounded-md bg-[#e91e63] text-white hover:bg-[#c2185b]"> 예약 확정 </button> </HStack> <HStack spacing={4} justify="center"> <button className="px-8 py-3 rounded-md border-2 border-[#e91e63] text-[#e91e63] hover:bg-[#e91e63]/10"> 예약 변경 </button> <button className="px-8 py-3 rounded-md bg-gradient-to-r from-[#e91e63] to-[#9c27b0] text-white hover:opacity-90"> 예약하기 </button> </HStack>

통계 카드 수평 스택

import { HStack } from "@vortex/ui"; <HStack spacing={6}> <div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#e91e63] to-[#c2185b] text-white"> <p className="text-sm opacity-90 mb-1">오늘 예약</p> <p className="text-4xl font-bold">12</p> </div> <div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#03a9f4] to-[#0288d1] text-white"> <p className="text-sm opacity-90 mb-1">대기 중</p> <p className="text-4xl font-bold">3</p> </div> <div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#9c27b0] to-[#7b1fa2] text-white"> <p className="text-sm opacity-90 mb-1">이번 주</p> <p className="text-4xl font-bold">48</p> </div> </HStack>;

브랜드별 비교

특성FoundationiCignalCals
목적범용 스택제품 정보, 버튼 그룹예약 정보, 폼 레이아웃
디자인중립적다크 테마밝고 친근한
Primary-Blue #0066ccPink #e91e63
기본 간격4 (1rem)4 (1rem)4 (1rem)
주요 사용범용 레이아웃제품 상세, 네비게이션예약 정보, 고객 정보
특화 기능-다크 모드상태별 시각적 구분

Props API

VStack / HStack

PropTypeDefaultDescription
spacingnumber4항목 간 간격 (Tailwind spacing 단위)
align’start’ | ‘center’ | ‘end’ | ‘stretch''stretch’교차축 정렬
justify’start’ | ‘center’ | ‘end’ | ‘between’ | ‘around''start’주축 정렬
classNamestring-추가 CSS 클래스
childrenReactNode-스택 항목들

Stack (제네릭)

PropTypeDefaultDescription
direction’vertical’ | ‘horizontal''vertical’스택 방향
spacingnumber4항목 간 간격
align’start’ | ‘center’ | ‘end’ | ‘stretch''stretch’교차축 정렬
justify’start’ | ‘center’ | ‘end’ | ‘between’ | ‘around''start’주축 정렬
classNamestring-추가 CSS 클래스
childrenReactNode-스택 항목들

접근성

  • 시맨틱 HTML: 리스트 항목은 <ul>, <ol>, <li> 사용
  • 키보드 네비게이션: 인터랙티브 요소는 적절한 tabIndex 설정
  • ARIA 속성: 복잡한 스택은 role 및 관련 속성 사용
  • 반응형: 모바일에서는 수평 스택이 수직으로 변경될 수 있도록 고려
{ /* 접근성 개선 예제 */ } <VStack spacing={4} as="ul" role="list" aria-label="예약 목록"> <li className="p-4 border rounded"> <span>예약 1</span> </li> <li className="p-4 border rounded"> <span>예약 2</span> </li> <li className="p-4 border rounded"> <span>예약 3</span> </li> </VStack>;

예제

1. 예약 상세 정보 스택

import { VStack, HStack } from "@vortex/ui"; import { Calendar, Clock, User, Phone, Mail, MapPin } from "lucide-react"; export default function ReservationDetailStack() { return ( <VStack spacing={6} className="p-6 border-l-4 border-[#03a9f4] bg-[#03a9f4]/5 rounded-r-lg" > {/* 헤더 */} <div> <h2 className="text-2xl font-bold text-[#03a9f4] mb-1">예약 확정</h2> <p className="text-sm text-muted-foreground"> 예약 번호: #RSV-20240115-001 </p> </div> {/* 서비스 정보 */} <VStack spacing={3}> <h3 className="text-lg font-semibold">서비스 정보</h3> <HStack spacing={3} align="center"> <Calendar className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">날짜</span> <span className="font-medium">2024년 1월 15일</span> </VStack> </HStack> <HStack spacing={3} align="center"> <Clock className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">시간</span> <span className="font-medium">오후 2:00 - 3:00</span> </VStack> </HStack> <HStack spacing={3} align="center"> <MapPin className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">장소</span> <span className="font-medium">강남점</span> </VStack> </HStack> </VStack> {/* 고객 정보 */} <VStack spacing={3}> <h3 className="text-lg font-semibold">고객 정보</h3> <HStack spacing={3} align="center"> <User className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">이름</span> <span className="font-medium">홍길동</span> </VStack> </HStack> <HStack spacing={3} align="center"> <Phone className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">연락처</span> <span className="font-medium">010-1234-5678</span> </VStack> </HStack> <HStack spacing={3} align="center"> <Mail className="w-5 h-5 text-muted-foreground" /> <VStack spacing={0}> <span className="text-sm text-muted-foreground">이메일</span> <span className="font-medium">hong@example.com</span> </VStack> </HStack> </VStack> {/* 결제 정보 */} <VStack spacing={2} className="p-4 bg-white rounded-lg"> <HStack justify="between"> <span className="text-muted-foreground">서비스 금액</span> <span className="font-medium">30,000원</span> </HStack> <HStack justify="between"> <span className="text-muted-foreground">할인</span> <span className="font-medium text-[#f44336]">-3,000원</span> </HStack> <div className="border-t pt-2"> <HStack justify="between"> <span className="font-semibold">총 결제 금액</span> <span className="text-xl font-bold text-[#e91e63]">27,000원</span> </HStack> </div> </VStack> {/* 액션 버튼 */} <HStack spacing={4} justify="end"> <button className="px-6 py-2 rounded-md border border-gray-300 hover:bg-gray-100"> 예약 변경 </button> <button className="px-6 py-2 rounded-md bg-[#f44336] text-white hover:bg-[#d32f2f]"> 예약 취소 </button> </HStack> </VStack> ); }

2. 폼 레이아웃 스택

import { VStack, HStack } from "@vortex/ui"; export default function ReservationFormStack() { return ( <VStack spacing={6} className="max-w-2xl mx-auto p-6"> <div> <h1 className="text-3xl font-bold text-[#e91e63] mb-2"> 새 예약 만들기 </h1> <p className="text-muted-foreground">예약 정보를 입력해주세요.</p> </div> {/* 고객 정보 */} <VStack spacing={4} className="p-6 bg-white rounded-lg border"> <h2 className="text-xl font-semibold">고객 정보</h2> <VStack spacing={2}> <label className="text-sm font-medium">이름 *</label> <input type="text" className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none" placeholder="홍길동" /> </VStack> <HStack spacing={4}> <VStack spacing={2} className="flex-1"> <label className="text-sm font-medium">연락처 *</label> <input type="tel" className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none" placeholder="010-1234-5678" /> </VStack> <VStack spacing={2} className="flex-1"> <label className="text-sm font-medium">이메일</label> <input type="email" className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none" placeholder="hong@example.com" /> </VStack> </HStack> </VStack> {/* 예약 정보 */} <VStack spacing={4} className="p-6 bg-white rounded-lg border"> <h2 className="text-xl font-semibold">예약 정보</h2> <VStack spacing={2}> <label className="text-sm font-medium">서비스 선택 *</label> <select className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"> <option>헤어 커트</option> <option>헤어 펌</option> <option>헤어 염색</option> </select> </VStack> <HStack spacing={4}> <VStack spacing={2} className="flex-1"> <label className="text-sm font-medium">날짜 *</label> <input type="date" className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none" /> </VStack> <VStack spacing={2} className="flex-1"> <label className="text-sm font-medium">시간 *</label> <select className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"> <option>10:00</option> <option>11:00</option> <option>14:00</option> <option>15:00</option> </select> </VStack> </HStack> <VStack spacing={2}> <label className="text-sm font-medium">요청사항</label> <textarea rows={4} className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none resize-none" placeholder="요청사항을 입력해주세요" /> </VStack> </VStack> {/* 제출 버튼 */} <HStack spacing={4} justify="end"> <button className="px-8 py-3 rounded-md border border-gray-300 hover:bg-gray-100"> 취소 </button> <button className="px-8 py-3 rounded-md bg-gradient-to-r from-[#e91e63] to-[#9c27b0] text-white hover:opacity-90"> 예약하기 </button> </HStack> </VStack> ); }

3. 예약 목록 스택

import { VStack, HStack } from "@vortex/ui"; import { Calendar, Clock, User, MoreVertical } from "lucide-react"; const reservations = [ { id: 1, status: "confirmed", customer: "홍길동", service: "헤어 커트", time: "14:00", date: "2024-01-15", }, { id: 2, status: "pending", customer: "김고객", service: "헤어 펌", time: "15:00", date: "2024-01-15", }, { id: 3, status: "confirmed", customer: "이고객", service: "헤어 염색", time: "16:00", date: "2024-01-15", }, ]; const statusConfig = { pending: { color: "#ff9800", bg: "bg-[#ff9800]/10", label: "대기 중" }, confirmed: { color: "#03a9f4", bg: "bg-[#03a9f4]/10", label: "확정됨" }, }; export default function ReservationListStack() { return ( <VStack spacing={4}> <div className="flex items-center justify-between"> <h2 className="text-2xl font-bold">오늘의 예약</h2> <button className="px-4 py-2 rounded-md bg-[#e91e63] text-white hover:bg-[#c2185b]"> 새 예약 </button> </div> <VStack spacing={3}> {reservations.map((reservation) => { const config = statusConfig[reservation.status]; return ( <div key={reservation.id} className={`p-6 rounded-lg border-l-4 ${config.bg} cursor-pointer hover:shadow-md transition-shadow`} style={{ borderLeftColor: config.color }} > <HStack justify="between" align="start"> <VStack spacing={3} className="flex-1"> <HStack spacing={3} align="center"> <div className="px-3 py-1 rounded-full text-xs font-medium text-white" style={{ backgroundColor: config.color }} > {config.label} </div> <span className="text-lg font-semibold"> {reservation.service} </span> </HStack> <HStack spacing={6}> <HStack spacing={2} align="center"> <User className="w-4 h-4 text-muted-foreground" /> <span className="text-sm">{reservation.customer}</span> </HStack> <HStack spacing={2} align="center"> <Calendar className="w-4 h-4 text-muted-foreground" /> <span className="text-sm">{reservation.date}</span> </HStack> <HStack spacing={2} align="center"> <Clock className="w-4 h-4 text-muted-foreground" /> <span className="text-sm font-medium" style={{ color: config.color }} > {reservation.time} </span> </HStack> </HStack> </VStack> <button className="p-2 hover:bg-gray-100 rounded-md"> <MoreVertical className="w-5 h-5 text-muted-foreground" /> </button> </HStack> </div> ); })} </VStack> </VStack> ); }

관련 컴포넌트

  • Card - 카드 컴포넌트
  • Container - 페이지 레이아웃 컨테이너
  • Grid - 그리드 레이아웃
  • Button - 액션 버튼
Last updated on