इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

Tabs

Tabs एक ही जगह के सेक्शन के बीच स्विच करते हैं: किसी प्रोजेक्ट का ओवरव्यू और एक्टिविटी, या किसी दस्तावेज़ के व्यू। हर Tab एक टैब का व्यवहार संभालता है: प्रेस, कीबोर्ड एक्टिवेशन, disabled स्थिति, और स्क्रीन रीडर के लिए टैब रोल और उसकी selected स्थिति। इनके चारों ओर का Tabs टैब लिस्ट है, जिसे स्क्रीन रीडर एक सेट के रूप में बोलते हैं। लुक ऐप का है: टैब के अपने कोई रंग, साइज़ या लेबल नहीं होते। उसके चिल्ड्रन ऐप का लेबल हैं, और टैब पर selected: क्लासेस, या उसके अंदर की चीज़ों पर group-selected: क्लासेस, उसकी स्थिति के अनुसार लागू होती हैं। हर सेक्शन का कंटेंट भी ऐप का है: लिस्ट के बगल में वही सेक्शन दिखता है जिसे उसकी value नाम देती है। लिस्ट का इंडिकेटर, यानी ऐप द्वारा स्टाइल की गई अंडरलाइन या पिल, selected टैब पर बैठता है और अगले टैब तक नेटिव रूप से सरकता है (Indicator)।

el-tabs.tsx
el-tabs.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Tab, Tabs } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, size, Split, tint, ui } from "./palette";

// What each section shows: the app switches the content by its own value.
const sections = [
  {
    id: "overview",
    label: "Overview",
    rows: [
      ["Visitors", "12,480"],
      ["Sign-ups", "342"],
      ["Conversion", "2.74%"],
    ],
  },
  {
    id: "activity",
    label: "Activity",
    rows: [
      ["Deploy to production", "09:41"],
      ["Invoice paid", "08:15"],
      ["New member joined", "07:02"],
    ],
  },
  {
    id: "settings",
    label: "Settings",
    rows: [
      ["Region", "Frankfurt"],
      ["Plan", "Pro"],
      ["Seats", "8 of 10"],
    ],
  },
];
const folders = [
  { id: "inbox", label: "Inbox", count: 12 },
  { id: "archive", label: "Archive", count: 0 },
  { id: "spam", label: "Spam", count: 3, locked: true },
];

function App() {
  const section = signal("overview");
  const folder = signal("inbox");
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 3));
  const current = () => sections.find((s) => s.id === section())!;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>tabs</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click a tab, or Tab to a list and move with the arrow keys" : "Tap a tab to show its section"}
      </Text>

      <Split gap={14} inspector={<Log title={`onPress · ${section()}, ${folder()}`} lines={events()} minHeight={76} />}>
        {/* A segmented tab list: each tab is a segment, and the list's indicator is the raised one, inside the
            list's padding, which springs to the tab the app selects. */}
        <Tabs
          style={{ flexDirection: "row", gap: 4, padding: 3, borderRadius: 10, ...ui().inset, ...(desktop ? { alignSelf: "flex-start" } : {}) }}
          indicatorStyle={{ top: 3, bottom: 3, borderRadius: 7, ...ui().raised }}
        >
          <Each of={sections}>
            {(s) => (
              <Tab
                selected={section() === s().id}
                onPress={() => {
                  section.set(s().id);
                  log(`${s().label} pressed`);
                }}
                style={
                  desktop
                    ? { height: 26, paddingHorizontal: 14, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                    : { flex: 1, height: 30, borderRadius: 7, alignItems: "center", justifyContent: "center" }
                }
                class="active:opacity-70"
                hoverStyle={section() === s().id ? undefined : hoverFill()}
                focusStyle={focusRing()}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: section() === s().id ? ui().text : ui().muted }}>
                  {s().label}
                </Text>
              </Tab>
            )}
          </Each>
        </Tabs>

        {/* The selected section's content, which the app shows by its value. */}
        <View style={{ ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          <Each of={current().rows}>
            {(row, i) => (
              <>
                {i() > 0 ? <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} /> : null}
                <View
                  style={
                    desktop
                      ? { flexDirection: "row", alignItems: "center", paddingHorizontal: 12, height: size.row }
                      : { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12 }
                  }
                >
                  <Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>{row()[0]}</Text>
                  <Text style={{ fontSize: 14, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().muted }}>{row()[1]}</Text>
                </View>
              </>
            )}
          </Each>
        </View>

        {/* Tabs with counts: the selected one's own fill, a disabled one dimmed. */}
        <Tabs style={{ flexDirection: "row", gap: 6 }}>
          <Each of={folders}>
            {(f) => (
              <Tab
                selected={folder() === f().id}
                disabled={f().locked}
                onPress={() => {
                  folder.set(f().id);
                  log(`${f().label} pressed`);
                }}
                class="active:opacity-70 disabled:opacity-40"
                hoverStyle={f().locked || folder() === f().id ? undefined : hoverFill()}
                focusStyle={focusRing()}
                style={{ flexDirection: "row", alignItems: "center", gap: 6, height: desktop ? size.controlSm : 30, paddingHorizontal: desktop ? 10 : 12, borderRadius: 8 }}
                selectedStyle={{ backgroundColor: tint(ui().accent, 0.16) }}
              >
                <Text style={{ fontSize: 13, fontWeight: "500", color: folder() === f().id ? ui().accent : ui().muted }}>
                  {f().label}
                </Text>
                {f().count > 0 ? (
                  <Text style={{ fontSize: 12, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().faint }}>{f().count}</Text>
                ) : null}
              </Tab>
            )}
          </Each>
        </Tabs>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{tab, tabs, theme};
use nsh_core::prelude::*;

fn sections() -> impl IntoElement {
    let section = signal("overview");
    let item = move |value: &'static str, label: &'static str| {
        let selected = move || section.get() == value;
        tab()
            .selected(selected)
            .on_press(move |_: &()| section.set(value))
            .rounded(move || theme().radius)
            .px_3()
            .py_2()
            .child(text(label).text_sm().text_color(move || {
                if selected() { theme().foreground } else { theme().muted_foreground }
            }))
    };
    view()
        .gap_4()
        .child(
            tabs()
                .flex_row()
                .self_start()
                .gap_1()
                .p_1()
                .rounded(move || theme().radius)
                .bg(move || theme().muted)
                .indicator_style(|s| {
                    s.top_1()
                        .bottom_1()
                        .rounded(move || theme().radius)
                        .bg(move || theme().background)
                })
                .child(item("overview", "Overview"))
                .child(item("activity", "Activity")),
        )
        .child(text(move || {
            if section.get() == "overview" {
                "Visitors, sign-ups and conversion this week."
            } else {
                "Deploys, invoices and new members, newest first."
            }
        }).text_color(move || theme().foreground))
}

Tab और Tabs nsh/base से आते हैं, जो Rust में nsh-base crate है। value ऐप की है: लिस्ट में कोई value नहीं होती। हर टैब को बताया जाता है कि वह selected है या नहीं; प्रेस onPress रिपोर्ट करता है, ऐप उसकी value सेट करती है और वह सेक्शन दिखाती है, और हर टैब वही selected दिखाता है जो ऐप वापस देती है। लिस्ट और टैब के कोने कॉन्सेंट्रिक हैं: लिस्ट की पैडिंग (p-1) radius.lg और radius का अंतर है। selected टैब का फ़िल लिस्ट का इंडिकेटर है (indicatorClass, उस पैडिंग के अंदर: inset-y-1), जो ऐप द्वारा चुने गए टैब तक स्प्रिंग की तरह जाता है। Rust में tab() और tabs() इन्हें बनाते हैं, जिनमें हर prop के लिए एक सेटर (.selected(..)), हर इवेंट के लिए एक लिसनर (.on_press(..)), टाइप्ड स्टाइल मेथड और उनके कंटेंट के लिए .child(..) होता है।

#Props

#Tab

प्रॉपRustटाइपडिफ़ॉल्टविवरण
selected.selected(..)booleanfalseअपनी लिस्ट का मौजूदा टैब: स्क्रीन रीडर इसे selected बोलते हैं, और selected: स्टाइल लागू होती हैं। प्रेस onPress रिपोर्ट करता है; टैब वही value दिखाता है जो ऐप वापस देती है।
disabled.disabled(..)booleanfalseकोई प्रेस और प्रेस फ़ीडबैक नहीं; disabled: स्टाइल लागू होती हैं, और स्क्रीन रीडर इसे dimmed बोलते हैं।
selectedStyle.selected_style(|s| ..)StyleProp<ViewStyle>कोई नहींselected और not disabled होने पर टैब की स्टाइल, selected: क्लासेस के रूप में।
accessibilityLabel.accessibility_label(..)stringकोई नहींस्क्रीन रीडर क्या बोलते हैं: टैब का नाम, जब उसके चिल्ड्रन में ऐसा कोई टेक्स्ट न हो जो यह बताए (जैसे आइकन)।
children.child(..)JSX.Elementकोई नहींऐप का लेबल, आइकन या बैज, जिसे स्क्रीन रीडर बोलते हैं।
class, styleस्टाइल मेथडstring, StyleProp<ViewStyle>कोई नहींटैब खुद: उसके लेबल के चारों ओर की सतह।
activeStyle, hoverStyle, focusStyle, disabledStyle.active(|s| ..), .hover(..), .focus(..), .disabled_style(..)StyleProp<ViewStyle>कोई नहींpressed, hovered, focused या disabled होने पर टैब की स्टाइल, <state>: क्लासेस के रूप में।

#Tabs

प्रॉपRustटाइपडिफ़ॉल्टविवरण
indicatorClass.indicator_class(..)stringकोई नहींselected टैब पर इंडिकेटर की Tailwind क्लासेस: उसका लुक और लिस्ट के आर-पार उसकी जगह (रो में अंडरलाइन के लिए bottom-0 h-0.5, पिल के लिए inset-y-1)। लिस्ट के अनुदिश यह selected टैब को कवर करता है, उस अक्ष पर अपने मार्जिन के अंदर। indicatorClass या indicatorStyle होने पर लिस्ट में एक इंडिकेटर होता है।
indicatorStyle.indicator_style(|s| ..)StyleProp<ViewStyle>कोई नहींइंडिकेटर की स्टाइल, indicatorClass के ऊपर।
children.child(..)JSX.Elementकोई नहींलिस्ट के टैब, और उनके बीच ऐप जो कुछ भी रखे।
class, styleस्टाइल मेथडstring, StyleProp<ViewStyle>कोई नहींलिस्ट का लेआउट और लुक: डिफ़ॉल्ट रूप से कॉलम, रो के लिए flex-row।

#Events

इवेंटRustविवरण
onPress (Tab).on_press(|_: &()| ..)टैब प्रेस किया गया: टैप, क्लिक, कीबोर्ड फ़ोकस होने पर Space या Return, या स्क्रीन रीडर का एक्टिवेशन; या उसकी लिस्ट की एक एरो की, Home या End ने कीबोर्ड फ़ोकस को उस पर पहुँचाया। यह कुछ भी बदलने से पहले चलता है: टैब वही selected दिखाता है जो ऐप वापस देती है।

selected टैब भी अपने प्रेस रिपोर्ट करता है, ताकि ऐप दूसरे प्रेस पर कोई कार्रवाई कर सके (जैसे अपने सेक्शन को ऊपर तक स्क्रॉल करना)। disabled टैब कुछ रिपोर्ट नहीं करता।

#Parts

पार्टअंदरस्टाइल का तरीकास्थितियाँ
लिस्टTabs के class, style
इंडिकेटरलिस्ट, टैब के पीछेindicatorClass, indicatorStyleselected टैब पर
टैबलिस्टTab के class, styleselected:, disabled:, active:, hover:, focus: (या selectedStyle, disabledStyle, …)
लेबलटैबउसके अपने एलिमेंट (children)group-selected:, group-disabled: और अन्य group-*: वैरिएंट, टैब के class पर group के साथ
सेक्शन का कंटेंटलिस्ट के बगल मेंऐप के एलिमेंटऐप की value

selected टैब को उसकी अपनी सतह से दिखाएँ: फ़िल, गहरा लेबल, भारी वेट (Design guides देखें)।

#Indicator

selected टैब के नीचे की अंडरलाइन, उसके पीछे की पिल या सेगमेंटेड कंट्रोल का उभरा हुआ सेगमेंट लिस्ट का इंडिकेटर है: एक व्यू जिसे ऐप indicatorClass और indicatorStyle से स्टाइल करती है, जो टैब के पीछे बनता है और लिस्ट में absolutely positioned होता है। Native Shell हर लेआउट के बाद इसे selected टैब पर रखता है। लिस्ट के अनुदिश (रो की चौड़ाई, कॉलम की ऊँचाई) यह उस टैब को कवर करता है, उस अक्ष पर इंडिकेटर के अपने मार्जिन के अंदर (टैब से छोटी अंडरलाइन के लिए mx-3); लिस्ट के आर-पार उसकी क्लासेस उसकी जगह तय करती हैं:

// An underline at the bottom of a row of tabs.
<Tabs class="flex-row gap-4 border-b border-border" indicatorClass="bottom-0 h-0.5 bg-foreground">
  {item("overview", "Overview")}
  {item("activity", "Activity")}
</Tabs>

// A pill behind the selected tab, inside the list's padding.
<Tabs class="flex-row self-start gap-1 rounded-(--radius-lg) bg-muted p-1" indicatorClass="inset-y-1 rounded-(--radius) bg-background shadow-sm">
  {item("overview", "Overview")}
  {item("activity", "Activity")}
</Tabs>

जब ऐप कोई दूसरा टैब चुनती है, इंडिकेटर नेटिव रूप से वहाँ स्प्रिंग की तरह जाता है, जैसे GPUI Kit का tab bar indicator; reduced motion में वह तुरंत चला जाता है। जब कोई टैब selected नहीं होता तो यह छिपा रहता है; जब कई होते हैं तो यह पहले पर रहता है। रीसाइज़ या किसी टैब की चौड़ाई में बदलाव इसे उसी फ़्रेम में टैब के साथ सरका देता है। इंडिकेटर को किसी लेआउट इवेंट या ऐप के किसी कोड की ज़रूरत नहीं: selected टैब वही है जिसका selected true है। स्क्रॉल होने वाली लिस्ट में स्क्रॉल व्यू Tabs के चारों ओर जाता है, और इंडिकेटर टैब के साथ स्क्रॉल होता है।

#Disabled

disabled टैब कोई प्रेस नहीं लेता, और स्क्रीन रीडर उसे dimmed बोलते हैं। उसकी disabled: क्लासेस उसकी selected: क्लासेस के ऊपर लागू होती हैं, और उसके अंदर की group-disabled: क्लासेस उसका अनुसरण करती हैं:

<Tab
  selected={section() === "billing"}
  disabled={!isOwner()}
  onPress={() => section.set("billing")}
  class="group rounded-(--radius) px-3 py-2 selected:bg-background disabled:opacity-50"
>
  <Text class="text-sm text-muted-foreground group-selected:text-foreground">Billing</Text>
</Tab>

#Accessibility

  • स्क्रीन रीडर के लिए टैब एक एलिमेंट है, जिसका रोल "tab" है और साथ में उसकी selected स्थिति। उसका लेबल उसके चिल्ड्रन का टेक्स्ट है, या accessibilityLabel।
  • Tabs का रोल "tablist" है: स्क्रीन रीडर उसके अंदर के टैब को एक सेट के रूप में सुनाते हैं, हर टैब फिर भी अपना एक अलग एलिमेंट रहता है।
  • disabled को वैसे ही बोला जाता है जैसे प्लेटफ़ॉर्म बोलता है (VoiceOver, TalkBack, UI Automation, ब्राउज़र का accessibility tree)।
  • macOS और वेब पर लिस्ट कीबोर्ड फ़ोकस का एक स्टॉप है, और एरो की उसके टैब के बीच चलती हैं (नीचे देखें)।
  • इंडिकेटर स्क्रीन रीडर से छिपा है और फ़ोकस नहीं लेता: लिस्ट के एलिमेंट केवल टैब हैं।

सामान्य रूप से लेबल, रोल और एनाउंसमेंट के लिए Accessibility देखें।

#Keyboard

macOS और वेब पर, जहाँ Tab उन व्यू तक पहुँचता है जो प्रेस लेते हैं, Tabs लिस्ट WAI-ARIA tabs pattern का पालन करती है:

कीक्या करती है
Tab, Shift+Tabलिस्ट में selected टैब पर प्रवेश कराती है (कोई selected न हो तो पहला टैब जो disabled नहीं है), और एक ही चरण में उससे बाहर ले जाती है।
Right, Downफ़ोकस को अगले टैब पर ले जाती है, आख़िरी से पहले पर।
Left, Upफ़ोकस को पिछले टैब पर ले जाती है, पहले से आख़िरी पर।
Home, Endफ़ोकस को पहले या आख़िरी टैब पर ले जाती है।
Space, Returnफ़ोकस वाले टैब को प्रेस करती है।

एरो की disabled टैब को छोड़ देती हैं, और जिस टैब पर वे पहुँचती हैं वह onPress रिपोर्ट करता है, इसलिए फ़ोकस के चलते ही ऐप उसे select कर लेती है: सेक्शन फ़ोकस के पीछे चलता है। लिस्ट का लेआउट कुछ भी हो, एरो के दोनों जोड़े चलते हैं; लेआउट ऐप की क्लास तय करती है। right-to-left लेआउट में रो दाईं ओर से चलती है, इसलिए Left और Right की भूमिकाएँ बदल जाती हैं: Left अगले टैब पर और Right पिछले पर ले जाती है; Up, Down, Home और End अपना काम वैसे ही करते हैं। focus: स्टाइल दिखाती हैं कि फ़ोकस कहाँ है। Tabs के बाहर का Tab अपना एक अलग स्टॉप है।

#Platform notes

  • iOS, Android और Mac Catalyst: प्रेस एक टैप है जो टैब पर ख़त्म होता है, उसके लेबल समेत।
  • डेस्कटॉप (macOS, Windows, Linux, GPUI) और वेब: प्रेस एक क्लिक है। macOS और वेब पर Space और Return भी कीबोर्ड फ़ोकस वाले टैब को प्रेस करते हैं, और एरो की, Home और End उसे चलाते हैं (Keyboard)।
  • हर प्लेटफ़ॉर्म वही बनाता है जो ऐप स्टाइल करती है: टैब, selected टैब की सतह, इंडिकेटर और सेक्शन का कंटेंट। इंडिकेटर की गति हर प्लेटफ़ॉर्म पर एक नेटिव लेआउट एनिमेशन है।