Tabs

Tabs

서로 다른 뷰 사이를 전환하는 탭 컴포넌트예요. 여러 콘텐츠 영역을 하나의 공간에 효율적으로 정리할 때 사용해요.

출처: 문서

본문

사용법 (Usage)

Tabs 컴포넌트는 Tabs.List, Tabs.Tab, Tabs.Panel 하위 컴포넌트로 구성돼요.

import { Tabs } from '@mantine/core';
import { ImageIcon, ChatCircleIcon, GearSixIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <Tabs defaultValue="gallery">
      <Tabs.List>
        <Tabs.Tab value="gallery" leftSection={<ImageIcon size={16} />}>
          Gallery
        </Tabs.Tab>
        <Tabs.Tab value="messages" leftSection={<ChatCircleIcon size={16} />}>
          Messages
        </Tabs.Tab>
        <Tabs.Tab value="settings" leftSection={<GearSixIcon size={16} />}>
          Settings
        </Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="gallery">Gallery tab content</Tabs.Panel>
      <Tabs.Panel value="messages">Messages tab content</Tabs.Panel>
      <Tabs.Panel value="settings">Settings tab content</Tabs.Panel>
    </Tabs>
  );
}

제어 컴포넌트 (Controlled Tabs)

Tabs 상태를 제어하려면 value와 onChange props를 사용해요.

import { useState } from 'react';
import { Tabs } from '@mantine/core';

function Demo() {
  const [activeTab, setActiveTab] = useState<string | null>('first');

  return (
    <Tabs value={activeTab} onChange={setActiveTab}>
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="first">First panel</Tabs.Panel>
      <Tabs.Panel value="second">Second panel</Tabs.Panel>
    </Tabs>
  );
}

비제어 컴포넌트 (Uncontrolled Tabs)

Tabs 상태 변경을 구독할 필요가 없다면 defaultValue를 사용해요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="first">
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="first">First panel</Tabs.Panel>
      <Tabs.Panel value="second">Second panel</Tabs.Panel>
    </Tabs>
  );
}

색상 변경 (Change colors)

모든 탭의 색상을 바꾸려면 Tabs 컴포넌트에 color를 설정하고, 개별 탭의 색상을 바꾸려면 Tabs.Tab에 color를 설정해요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs color="teal" defaultValue="first">
      <Tabs.List>
        <Tabs.Tab value="first">Teal tab</Tabs.Tab>
        <Tabs.Tab value="second" color="blue">Blue tab</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="first" pt="xs">First tab color is teal, it gets this value from context</Tabs.Panel>
      <Tabs.Panel value="second" pt="xs">
        Second tab color is blue, it gets this value from props, props have the priority and will override context value
      </Tabs.Panel>
    </Tabs>
  );
}

Tabs 위치 (Tabs position)

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="first">
      <Tabs.List grow justify="center">
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
        <Tabs.Tab value="third">Third tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

탭을 반대쪽에 표시하려면 ml="auto" prop이나 className으로 margin-left: auto를 설정해요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="chat">
      <Tabs.List>
        <Tabs.Tab value="chat">Chat</Tabs.Tab>
        <Tabs.Tab value="gallery">Gallery</Tabs.Tab>
        <Tabs.Tab value="settings">Settings</Tabs.Tab>
        <Tabs.Tab value="account" ml="auto">Account</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

반전 탭 (Inverted tabs)

탭을 반전하려면 Tabs.Panel 컴포넌트를 Tabs.List 앞에 배치하고 Tabs 컴포넌트에 inverted prop을 추가해요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="chat" inverted>
      <Tabs.Panel value="chat">Chat panel</Tabs.Panel>
      <Tabs.Panel value="gallery">Gallery panel</Tabs.Panel>
      <Tabs.Panel value="account">Account panel</Tabs.Panel>

      <Tabs.List>
        <Tabs.Tab value="chat">Chat</Tabs.Tab>
        <Tabs.Tab value="gallery">Gallery</Tabs.Tab>
        <Tabs.Tab value="account">Account</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

세로 탭 배치 (Vertical tabs placement)

세로 방향에서 Tabs.List의 배치를 바꾸려면 placement prop을 설정해요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="gallery" orientation="vertical" placement="right">
      <Tabs.List>
        <Tabs.Tab value="gallery">Gallery</Tabs.Tab>
        <Tabs.Tab value="messages">Messages</Tabs.Tab>
        <Tabs.Tab value="settings">Settings</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="gallery">Gallery tab content</Tabs.Panel>
      <Tabs.Panel value="messages">Messages tab content</Tabs.Panel>
      <Tabs.Panel value="settings">Settings tab content</Tabs.Panel>
    </Tabs>
  );
}

커스텀 variants (Custom variants)

FloatingIndicator를 사용한 커스텀 variant 예시:

import { useState } from 'react';
import { FloatingIndicator, Tabs } from '@mantine/core';
import classes from './Demo.module.css';

function Demo() {
  const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
  const [value, setValue] = useState<string | null>('1');
  const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
  const setControlRef = (val: string) => (node: HTMLButtonElement) => {
    controlsRefs[val] = node;
    setControlsRefs(controlsRefs);
  };

  return (
    <div className={classes.root} ref={setRootRef}>
      <FloatingIndicator target={value ? controlsRefs[value] : null} parent={rootRef} />
      <Tabs value={value} onChange={setValue}>
        <Tabs.List ref={setRootRef}>
          <Tabs.Tab value="1" ref={setControlRef('1')}>First tab</Tabs.Tab>
          <Tabs.Tab value="2" ref={setControlRef('2')}>Second tab</Tabs.Tab>
          <Tabs.Tab value="3" ref={setControlRef('3')}>Third tab</Tabs.Tab>
        </Tabs.List>
        <Tabs.Panel value="1">First tab content</Tabs.Panel>
        <Tabs.Panel value="2">Second tab content</Tabs.Panel>
        <Tabs.Panel value="3">Third tab content</Tabs.Panel>
      </Tabs>
    </div>
  );
}

비활성 탭 (Disabled tabs)

Tabs.Tab 컴포넌트에 disabled prop을 설정하면 탭을 비활성화할 수 있어요. 비활성 탭은 마우스나 키보드로 활성화할 수 없고, 화살표 키로 내비게이션할 때 건너뛰어져요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="chat">
      <Tabs.List>
        <Tabs.Tab value="chat">Chat</Tabs.Tab>
        <Tabs.Tab value="gallery">Gallery</Tabs.Tab>
        <Tabs.Tab value="settings" disabled>Settings</Tabs.Tab>
        <Tabs.Tab value="account">Account</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

활성화 모드 (Activation mode)

기본적으로 탭은 사용자가 화살표 키나 Home/End 키를 누르면 활성화돼요. 이를 비활성화하려면 Tabs 컴포넌트에 activateTabWithKeyboard={false}를 설정해요.

탭 비활성화 (Tab deactivation)

기본적으로 활성 탭은 비활성화할 수 없어요. 이를 허용하려면 Tabs 컴포넌트에 allowTabDeactivation을 설정해요.

비활성 탭 언마운트 (Unmount inactive tabs)

기본적으로 비활성 Tabs.Panel은 마운트된 상태로 유지돼요. 비활성 탭을 언마운트하려면 Tabs에 keepMounted={false}를 설정해요. Tabs.Panel 안에 성능에 영향을 주는 컴포넌트를 렌더링할 때 유용해요. Tabs.Panel 안에 렌더링된 컴포넌트는 마운트될 때마다(탭이 바뀔 때) 상태가 초기화된다는 점에 주의해요.

import { Tabs } from '@mantine/core';

// 두 번째 탭 패널은 사용자가 두 번째 탭을 활성화할 때만 마운트돼요
function Demo() {
  return (
    <Tabs defaultValue="first" keepMounted={false}>
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="first">First panel</Tabs.Panel>
      <Tabs.Panel value="second">Second panel</Tabs.Panel>
    </Tabs>
  );
}

탭 컨트롤 ref 가져오기 (Get tab control ref)

import { useRef } from 'react';
import { Tabs } from '@mantine/core';

function Demo() {
  const secondTabRef = useRef<HTMLButtonElement>(null);

  return (
    <Tabs defaultValue="first">
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab ref={secondTabRef} value="second">Second tab</Tabs.Tab>
        <Tabs.Tab value="third">Third tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

renderRoot prop을 사용해 Tabs.Tab을 a 요소로 렌더링할 수 있어요. 그러면 새 탭에서 열기(오른쪽 클릭), 중간 클릭, 크롤러·접근성을 위한 href 같은 네이티브 링크 동작을 얻어요.

import { Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="home">
      <Tabs.List>
        <Tabs.Tab value="home" renderRoot={(props) => <a {...props} href="/home" />}>Home</Tabs.Tab>
        <Tabs.Tab value="about" renderRoot={(props) => <a {...props} href="/about" />}>About</Tabs.Tab>
        <Tabs.Tab value="contacts" renderRoot={(props) => <a {...props} href="/contacts" />}>Contacts</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

component prop을 사용해 탭을 링크로 렌더링할 수도 있어요.

renderRoot prop을 사용해 Tabs.Tab을 Next.js Link 컴포넌트로 렌더링해요. Next.js Link는 제네릭 컴포넌트이므로 component 대신 renderRoot prop이 필요해요.

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Tabs } from '@mantine/core';

function Demo() {
  const pathname = usePathname();

  return (
    <Tabs value={pathname}>
      <Tabs.List>
        <Tabs.Tab value="/home" renderRoot={(props) => <Link {...props} href="/home" />}>Home</Tabs.Tab>
        <Tabs.Tab value="/about" renderRoot={(props) => <Link {...props} href="/about" />}>About</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

renderRoot prop을 사용해 Tabs.Tab을 React Router의 NavLink나 Link 컴포넌트로 렌더링해요.

import { Link, Route, useLocation } from 'react-router-dom';
import { Tabs } from '@mantine/core';

// Route: <Route path="/tabs/:tabValue" element={<Demo />} />

function Demo() {
  const location = useLocation();

  return (
    <Tabs value={location.pathname}>
      <Tabs.List>
        <Tabs.Tab value="/first" renderRoot={(props) => <Link {...props} to="/first" />}>First tab</Tabs.Tab>
        <Tabs.Tab value="/second" renderRoot={(props) => <Link {...props} to="/second" />}>Second tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

react-router와 함께 사용 (Usage with react-router)

import { useNavigate, useParams } from 'react-router-dom';
import { Tabs } from '@mantine/core';

function Demo() {
  const navigate = useNavigate();
  const { tabValue } = useParams();

  return (
    <Tabs
      value={tabValue}
      onChange={(value) => navigate(`/tabs/${value}`)}
    >
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

Next.js Pages Router와 함께 사용 (Usage with Next.js Pages Router)

// For file /tabs/[activeTab].tsx
import { useRouter } from 'next/router';
import { Tabs } from '@mantine/core';

function Demo() {
  const router = useRouter();

  return (
    <Tabs
      value={router.query.activeTab}
      onChange={(value) => router.push(`/tabs/${value}`)}
    >
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

Next.js App Router와 함께 사용 (Usage with Next.js App Router)

App Router에서는 usePathname 훅으로 경로명에서 현재 탭 값을 가져오고, useRouter가 반환하는 router로 탭을 변경해요. 두 훅 모두 next/navigation에서 임포트해요. 이 훅들을 사용하는 컴포넌트는 Client Component여야 해요(파일 상단에 'use client' 지시문 추가).

// For file /tabs/[activeTab]/page.tsx
'use client';

import { usePathname, useRouter } from 'next/navigation';
import { Tabs } from '@mantine/core';

function Demo() {
  const router = useRouter();
  const pathname = usePathname();
  const activeTab = pathname.split('/').pop();

  return (
    <Tabs
      value={activeTab}
      onChange={(value) => router.push(`/tabs/${value}`)}
    >
      <Tabs.List>
        <Tabs.Tab value="first">First tab</Tabs.Tab>
        <Tabs.Tab value="second">Second tab</Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

Styles API

Tabs는 Styles API를 지원해요. classNames prop으로 컴포넌트의 내부 요소에 스타일을 추가할 수 있어요. 자세한 내용은 Styles API 문서를 참고해요.

selector 설명
root 루트 요소(Tabs 컴포넌트)
list 탭 목록(Tabs.List 컴포넌트)
panel 탭 콘텐츠 패널(Tabs.Panel 컴포넌트)
tab 탭 버튼(Tabs.Tab 컴포넌트)
tabLabel Tabs.Tab의 라벨
tabSection Tabs.Tab의 왼쪽과 오른쪽 섹션

Styles API로 탭 스타일을 커스터마이즈하는 예시:

.tab {
  position: relative;
  border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
  background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));

  &:first-of-type {
    border-radius: 4px 0 0 4px;

    @mixin rtl {
      border-radius: 0 4px 4px 0;
    }
  }

  &:last-of-type {
    border-radius: 0 4px 4px 0;

    @mixin rtl {
      border-radius: 4px 0 0 4px;
    }
  }

  & + & {
    border-left-width: 0;

    @mixin rtl {
      border-right-width: 0;
      border-left-width: 1px;
    }
  }

  @mixin hover {
    background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
  }

  &[data-active] {
    z-index: 1;
    background-color: var(--mantine-color-blue-filled);
    border-color: var(--mantine-color-blue-filled);
    color: var(--mantine-color-white);

    @mixin hover {
      background-color: var(--mantine-color-blue-filled-hover);
    }
  }
}

Scroller 컴포넌트와 함께 사용 (Example with Scroller component)

Scroller 컴포넌트를 사용하면 사용 가능한 공간에 맞지 않는 많은 탭이 있을 때 탭 목록을 스크롤 가능하게 만들 수 있어요.

import { Scroller, Tabs } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="first">
      <Scroller>
        <Tabs.List>
          <Tabs.Tab value="first">First tab</Tabs.Tab>
          <Tabs.Tab value="second">Second tab</Tabs.Tab>
          {/* ...더 많은 탭 */}
          <Tabs.Tab value="tenth">Tenth tab</Tabs.Tab>
        </Tabs.List>
      </Scroller>
    </Tabs>
  );
}

접근성 (Accessibility)

Tabs 컴포넌트는 접근성 측면에서 WAI-ARIA 권장 사항을 따르는 것을 목표로 해요.

Tabs.Tab이 링크(a 태그)로 렌더링되면 role="tab" 속성이 암시적 링크 role을 덮어써요. 화면 판독기는 요소를 "링크"가 아닌 "탭"으로 알려줘요. 이는 WAI-ARIA 사양에 따른 예상 동작이에요.

탭을 Tabs.Panel 컴포넌트 없이 내비게이션 링크로 사용하면, 각 탭의 aria-controls 속성은 DOM에 존재하지 않는 panel 요소를 참조해요. 대부분의 화면 판독기는 이를 우아하게 처리하므로 경미한 접근성 고려사항이에요.

텍스트 콘텐츠 없이 아이콘만 있는 Tabs.Tab을 사용한다면 aria-label을 설정하거나 VisuallyHidden 컴포넌트를 사용해요.

import { CoinIcon } from '@phosphor-icons/react';
import { Tabs, VisuallyHidden } from '@mantine/core';

function Demo() {
  return (
    <Tabs defaultValue="chat">
      <Tabs.List>
        {/* aria-label 불필요, 탭이 children으로 라벨링돼요 */}
        <Tabs.Tab value="chat">Chat</Tabs.Tab>

        {/* aria-label 필요, 탭이 children으로 라벨링되지 않아요 */}
        <Tabs.Tab value="money" aria-label="Get money" />

        {/* aria-label 대신 VisuallyHidden을 사용할 수 있어요 */}
        <Tabs.Tab value="coins">
          <CoinIcon size={16} />
          <VisuallyHidden>Get coins</VisuallyHidden>
        </Tabs.Tab>
      </Tabs.List>
    </Tabs>
  );
}

탭 목록 라벨을 설정하려면 Tabs.List 컴포넌트에 aria-label을 설정해요. 화면 판독기가 이를 알려줘요.

키보드 상호작용 (Keyboard interactions)

키 (Key) 설명 (Description) 조건 (Condition)
ArrowRight 비활성이 아닌 다음 탭에 포커스를 두고 활성화해요 orientation="horizontal"
ArrowLeft 비활성이 아닌 이전 탭에 포커스를 두고 활성화해요 orientation="horizontal"
ArrowDown 비활성이 아닌 다음 탭에 포커스를 두고 활성화해요 orientation="vertical"
ArrowUp 비활성이 아닌 이전 탭에 포커스를 두고 활성화해요 orientation="vertical"
Home 첫 번째 탭에 포커스를 두고 활성화해요 –
End 마지막 탭에 포커스를 두고 활성화해요 –

더 알아보기 (Learn more)