Skyroc Native UI

Tabs

标签页,支持手势翻页与面板懒加载

标签页(Tabs)用 items 一次描述标签与面板:上方是可横向滚动的标签栏(指示器随激活项平滑移动),下方是面板区。原生端默认用 react-native-pager-view 提供手势翻页,web 回退到只切 display 的实现。

import { Tabs } from '@skyroc/native-ui';

基础用法

items 的每一项由 key / title / children 组成,默认是 line 型(底部一条主题色指示器)。

TabsBasic.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = [
  {
    children: (
      <Panel
        description="line 型指示器贴在 tabBar 底部,宽度跟随激活项"
        title="推荐"
      />
    ),
    key: 'recommend',
    title: '推荐'
  },
  {
    children: (
      <Panel
        description="左右滑动面板即可切换,指示器与滚动位置会跟着动"
        title="关注"
      />
    ),
    key: 'following',
    title: '关注'
  },
  {
    children: (
      <Panel
        description="点击 tab 与手势滑动共用同一份激活索引"
        title="热榜"
      />
    ),
    key: 'hot',
    title: '热榜'
  }
];

const TabsBasic = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs items={ITEMS} />
      </View>
    </View>
  );
};

export { TabsBasic };

Tabs 根节点是 flex-1,需要父容器给出确定高度。

何时使用

  • 同层级内容的分组切换,且分组数量固定。
  • 页面级的顶部标题栏用 NavBar;左侧竖排分类用 Sidebar

Pill 型

type="pill" 把指示器改成撑满 tab 高度的选中背景,标签栏本身是一块圆角灰底,标签平分宽度。

TabsPill.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = [
  {
    children: (
      <Panel
        description="pill 型指示器撑满 tab 高度,作为选中态背景"
        title="全部"
      />
    ),
    key: 'all',
    title: '全部'
  },
  {
    children: (
      <Panel
        description="tabBar 自身带底色与内边距,tab 等分宽度"
        title="进行中"
      />
    ),
    key: 'ongoing',
    title: '进行中'
  },
  {
    children: (
      <Panel
        description="指示器先于文字渲染,靠绘制顺序压在下层"
        title="已完成"
      />
    ),
    key: 'done',
    title: '已完成'
  }
];

const TabsPill = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          items={ITEMS}
          type="pill"
        />
      </View>
    </View>
  );
};

export { TabsPill };

可滚动标签栏

标签超出一屏时标签栏横向滚动,切换后会把激活项滚到视口中间。

TabsScrollable.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = [
  '前端工程',
  '客户端',
  '人工智能',
  '后端开发',
  '数据分析',
  '音视频',
  '安全攻防',
  '产品设计'
].map(name => ({
  children: (
    <Panel
      description="tab 总宽超出容器时 tabBar 可横向滚动,激活项会自动居中"
      title={name}
    />
  ),
  key: name,
  title: name
}));

const TabsScrollable = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          defaultActiveIndex={4}
          items={ITEMS}
        />
      </View>
    </View>
  );
};

export { TabsScrollable };

指示器是先于标签渲染的,靠绘制顺序压在文字下层 —— 负 zIndex 在 Android 上不可靠。

禁用项

items[i].disabled 让该标签不可点,并降到 40% 不透明度。手势滑到禁用页时会沿滑动方向回弹到最近的可用页。

TabsDisabled.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = [
  {
    children: (
      <Panel
        description="从这里向右滑,会越过被禁用的「审核中」"
        title="草稿"
      />
    ),
    key: 'draft',
    title: '草稿'
  },
  {
    children: (
      <Panel
        description="不会被展示"
        title="审核中"
      />
    ),
    disabled: true,
    key: 'reviewing',
    title: '审核中'
  },
  {
    children: (
      <Panel
        description="滑动落到禁用页时,会沿滑动方向回弹到最近的可用页"
        title="已发布"
      />
    ),
    key: 'published',
    title: '已发布'
  },
  {
    children: (
      <Panel
        description="不会被展示"
        title="已下架"
      />
    ),
    disabled: true,
    key: 'archived',
    title: '已下架'
  }
];

const TabsDisabled = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs items={ITEMS} />
      </View>
    </View>
  );
};

export { TabsDisabled };

受控模式

activeIndex + onIndexChange 把激活项交给外部状态;非受控用 defaultActiveIndex

TabsControlled.tsx
Loading…
import { Button, Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = ['第一步', '第二步', '第三步'].map((name, index) => ({
  children: (
    <Panel
      description={`当前是第 ${index + 1} 步,索引完全由外部 state 决定`}
      title={name}
    />
  ),
  key: name,
  title: name
}));

const TabsControlled = () => {
  const [step, setStep] = useState(0);

  return (
    <View className="gap-4 bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          activeIndex={step}
          items={ITEMS}
          type="pill"
          onIndexChange={setStep}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          color="secondary"
          variant="outline"
          onPress={() => setStep(value => Math.max(0, value - 1))}
        >
          上一步
        </Button>
        <Button
          color="primary"
          variant="tonal"
          onPress={() => setStep(value => Math.min(ITEMS.length - 1, value + 1))}
        >
          下一步
        </Button>
        <Text className="text-sm text-muted-foreground">activeIndex:{step}</Text>
      </View>
    </View>
  );
};

export { TabsControlled };

懒加载

lazy 开启后只渲染当前面板(以及 lazyPreloadDistance 指定的前后各 N 个),其余渲染 renderLazyPlaceholder(默认是一个居中的加载指示器)。

TabsLazy.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { useRef } from 'react';
import { View } from 'react-native';

/** 懒加载面板属性 */
interface LazyPanelProps {
  /** 面板标题 */
  title: string;
}

/** 记录首次挂载时刻,用于验证「加载后常驻、切走不卸载」 */
const LazyPanel = (props: LazyPanelProps) => {
  const { title } = props;

  const mountedAtRef = useRef(new Date().toLocaleTimeString());

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-sm text-muted-foreground">挂载于 {mountedAtRef.current}</Text>
      <Text className="text-center text-xs text-muted-foreground">来回切换,时间不变说明面板没有被卸载重建</Text>
    </View>
  );
};

const ITEMS: TabItem[] = ['日报', '周报', '月报', '年报'].map(name => ({
  children: <LazyPanel title={name} />,
  key: name,
  title: name
}));

const TabsLazy = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          lazy
          items={ITEMS}
          lazyPreloadDistance={1}
          renderLazyPlaceholder={() => (
            <View className="flex-1 items-center justify-center">
              <Text className="text-sm text-muted-foreground">尚未加载</Text>
            </View>
          )}
        />
      </View>
    </View>
  );
};

export { TabsLazy };

已加载的面板常驻不卸载(keep-alive 语义):切走只是被外层容器隐藏,面板内部的 state 与滚动位置都会保留。

关闭手势翻页

swipeable={false} 后只能点击标签切换,面板改用「全部挂载、靠 display 切换」的实现。

TabsSwipeDisabled.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = ['概览', '明细'].map(name => ({
  children: (
    <Panel
      description="关闭 swipeable 后只能点击切换,面板改用 display 切换"
      title={name}
    />
  ),
  key: name,
  title: name
}));

const TabsSwipeDisabled = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          items={ITEMS}
          swipeable={false}
          type="pill"
        />
      </View>
    </View>
  );
};

export { TabsSwipeDisabled };

swipeable 会在两棵不同的树之间切换(PagerView ↔ 面板栈),导致面板整体重挂载 —— 请把它当作初始化配置,不要在运行时来回切。web 端没有手势翻页,这个属性在回退实现里是空操作。

样式覆盖

className 追加到根节点上,classNames 按 slot 细粒度覆盖:

slot作用位置
root根容器
tabBar标签栏外层(line 型的下边框在这里)
tabBarContent标签的横向容器
tab单个标签
tabText标签文字(激活态加粗变主题色)
indicator指示器
pager面板栈容器(仅非手势实现使用)
content单个面板容器
TabsCustomSlots.tsx
Loading…
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 面板占位内容属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-6">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const ITEMS: TabItem[] = ['设计', '研发', '测试'].map(name => ({
  children: (
    <Panel
      description="通过 classNames 覆写 tabBar / tab / tabText / indicator 各插槽"
      title={name}
    />
  ),
  key: name,
  title: name
}));

const TabsCustomSlots = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 overflow-hidden rounded-xl border border-border/60">
        <Tabs
          classNames={{
            indicator: 'h-1 bg-destructive',
            tab: 'px-8 py-4',
            tabBar: 'bg-muted/40',
            tabText: 'text-base'
          }}
          items={ITEMS}
        />
      </View>
    </View>
  );
};

export { TabsCustomSlots };

title 传节点时不会包 TextclassNames.tabText 也就不再生效,样式自行控制。

API

Tabs

属性说明类型默认值
items*Tab 项数据TabItem[]-
type类型风格'line' | 'pill''line'
activeIndex受控当前激活索引number-
defaultActiveIndex非受控初始激活索引number0
onIndexChange激活索引变化回调(index: number) => void-
swipeable是否开启手势滑动切换;仅原生生效,且应视为初始化配置booleantrue
lazy是否懒加载面板内容;面板一旦加载即常驻,切走不卸载booleanfalse
lazyPreloadDistance开启 lazy 时预加载当前 tab 前后 N 个面板number0
renderLazyPlaceholder未加载面板的占位内容,默认是居中的加载指示器() => ReactNode-
className根容器类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<TabsSlots>-
ref根节点 View 的 ref,用于 measure / 滚动定位Ref<View>-

类型

import type { TabItem, TabsProps, TabsSlots, TabsType } from '@skyroc/native-ui';

TabsType

标签页风格:line 为底部指示线,pill 为选中背景块。

'line' | 'pill'

TabsSlots

可通过 classNames 覆盖的 slot 名称。

'content' | 'indicator' | 'pager' | 'root' | 'tab' | 'tabBar' | 'tabBarContent' | 'tabText'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 TabsSlots。

Partial<Record<Slots, string>>

TabItem

单个 tab 的配置。

字段类型说明
key*string唯一标识。
title*ReactNode标签标题;字符串会自动包裹为 Text。
childrenReactNode面板内容。
disabledboolean是否禁用该 tab,手势滑过时会回弹。

包内还导出了 tabsVariants