aravint-ui-navigation
v1.0.24
Published
A reusable, dependency-free React + TypeScript tabs component with numbered badges, sticky tab bar, and mobile-responsive sizing — while preserving the original desktop/tablet look exactly.
Downloads
2,412
Readme
CustomTabs
A reusable, dependency-free React + TypeScript tabs component with numbered badges, sticky tab bar, and mobile-responsive sizing — while preserving the original desktop/tablet look exactly.
Features
- Controlled or uncontrolled usage (
value/onValueChangeordefaultValue) - Numbered badges per tab (optional)
- Optional icons per tab
- Disabled tab support
- Sticky tab bar (
position: sticky; top: 0) - Active-tab underline-hiding trick (
marginBottom: -1) to visually merge the active tab with its content panel - Responsive breakpoints for tablet (
≤768px) and phone (≤480px) that only affect sizing (padding, font-size, gap, badge size) — colors, borders, and desktop layout are untouched - On phones, tabs wrap to a new row instead of overflowing or forcing horizontal scroll
Installation
Copy CustomTabs.tsx into your project (e.g. src/components/CustomTabs.tsx). No extra dependencies — just React.
Basic Usage
import CustomTabs, { TabItem } from "@digitus-fci-oa/navigation";
const tabs: TabItem[] = [
{ id: "overview", label: "Overview", content: <OverviewPanel /> },
{ id: "details", label: "Details", content: <DetailsPanel /> },
{ id: "history", label: "History", content: <HistoryPanel />, disabled: true },
];
function App() {
const [activeTab, setActiveTab] = useState("overview");
return (
<div className="h-screen bg-gray-50 flex flex-col">
<div className="flex-1 min-h-0">
<CustomTabs
tabs={tabs}
value={activeTab}
onValueChange={setActiveTab}
showTabNumbers
className="h-full"
tabListClassName="bg-white"
/>
</div>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tabs | TabItem[] | — | Required. Array of tab definitions. |
| defaultValue | string | first tab's id | Initial active tab (uncontrolled mode). |
| value | string | — | Active tab id (controlled mode). Providing this switches the component to controlled. |
| onValueChange | (value: string) => void | — | Called when the user selects a tab. |
| className | string | "" | Class on the outer wrapper. |
| tabListClassName | string | "" | Class on the tab bar container. |
| tabButtonClassName | string | "" | Class on each tab button. |
| contentClassName | string | "" | Class on the content panel. |
| showTabNumbers | boolean | true | Show/hide the numbered badge on each tab. |
| showContent | boolean | true | Show/hide the content panel entirely (render tab bar only). |
| animated | boolean | true | Enables the opacity transition on the content panel. |
TabItem
| Field | Type | Description |
|---|---|---|
| id | string | Required. Unique tab identifier. |
| label | string | Required. Tab display text. |
| content | React.ReactNode | Content rendered when this tab is active. |
| disabled | boolean | Disables selection of this tab. |
| icon | React.ReactNode | Optional icon rendered before the label. |
Controlled vs Uncontrolled
- Uncontrolled: omit
value. The component manages its own active tab internally, seeded bydefaultValue(or the first tab). - Controlled: pass
value+onValueChange. You own the active-tab state; the component only reports selection events.
Styling Approach
Colors, borders, radii, and the sticky/active-tab layout tricks are set as inline styles and are the same across all screen sizes — this is what keeps the desktop/tablet appearance unchanged.
Only size-related properties (padding, font-size, gap, badge width/height) live in an injected <style> block using CSS classes (.ctabs-list, .ctabs-btn, .ctabs-badge, .ctabs-label), because inline styles can't respond to @media queries. This block ships inside the component, so no external CSS file is required.
Breakpoints
| Breakpoint | Behavior |
|---|---|
| Desktop (default) | Original sizing: 8px 20px padding, 14px font, 20px badges. |
| Tablet (≤768px) | Slightly reduced padding/font/badge size. Tab bar scrolls horizontally if tabs overflow. |
| Phone (≤480px) | Further reduced padding/font/badge size. Tab bar wraps to a new row instead of scrolling, so every label stays fully visible. |
To customize breakpoint values, edit the @media blocks inside the component's <style> tag directly.
Notes / Gotchas
marginBottom: -1on the active tab intentionally overlaps the tab bar's bottom border so the active tab visually "merges" into its content panel. This depends onalignItems: "flex-end"on the tab list — don't remove that when customizing.- The phone-only
flex-wrap: wrapsetsoverflow-x: visibleto explicitly undo the tablet breakpoint'soverflow-x: auto, since media query rules don't reset each other automatically. - If you have a large number of tabs, consider whether wrapping (phone behavior) or horizontal scroll suits your use case better, and adjust the
480pxblock accordingly.
