Segment Group
A shadcn-style segment group component built with Ark UI primitives.
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupUnderlineTabsDemo = () => (
<SegmentGroupRoot
className="max-w-xl border-b"
defaultValue="react"
variant="underline"
>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="svelte">Svelte</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupUnderlineTabsDemo;
Installation
npx shadcn@latest add @ark-cn/segment-groupInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/segment-group.tsx
"use client";
import {
SegmentGroup as SegmentGroupPrimitive,
useSegmentGroup,
useSegmentGroupContext,
} from "@ark-ui/react/segment-group";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export { useSegmentGroup, useSegmentGroupContext };
export type TabsVariant = "default" | "underline";
export const SegmentGroupRoot = ({
orientation = "horizontal",
variant = "default",
className,
...props
}: SegmentGroupPrimitive.RootProps & { variant?: TabsVariant }) => (
<SegmentGroupPrimitive.Root
data-slot="segment-group"
orientation={orientation}
data-variant={variant}
className={cn("group/segment-group", className)}
{...props}
/>
);
export const SegmentGroup = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"relative z-0 flex w-fit items-center justify-center gap-x-0.5 border border-transparent text-muted-foreground",
"group-data-[orientation=vertical]/segment-group:flex-col",
"group-data-[variant=default]/segment-group:rounded-lg group-data-[variant=default]/segment-group:bg-muted group-data-[variant=default]/segment-group:p-0.5 group-data-[variant=default]/segment-group:text-muted-foreground/72",
"group-data-[orientation=vertical]/segment-group:px-1 group-data-[orientation=horizontal]/segment-group:py-1",
"group-data-invalid/segment-group:border-destructive/36 dark:group-data-invalid/segment-group:border-destructive/50",
"group-data-invalid/segment-group:has-focus-visible:ring-[3px] group-data-invalid/segment-group:has-focus-visible:ring-destructive/20 dark:group-data-invalid/segment-group:has-focus-visible:ring-destructive/40",
className,
)}
data-slot="segment-group-item"
>
{children}
<SegmentGroupPrimitive.Indicator
className={cn(
"absolute left-(--left) h-(--height) w-(--width) transition-[width,height,left,top] duration-200 ease-in-out",
"group-data-[variant=default]/segment-group:top-(--top) group-data-[variant=default]/segment-group:right-(--left)",
"group-data-[variant=default]/segment-group:-z-1 group-data-[variant=default]/segment-group:rounded-md group-data-[variant=default]/segment-group:bg-background group-data-[variant=default]/segment-group:shadow-sm/5 group-data-[variant=default]/segment-group:dark:bg-input",
"group-data-[variant=underline]/segment-group:bottom-0 group-data-[variant=underline]/segment-group:z-10 group-data-[variant=underline]/segment-group:bg-primary",
"group-data-[variant=underline]/segment-group:group-data-[orientation=horizontal]/segment-group:h-0.5 group-data-[variant=underline]/segment-group:group-data-[orientation=horizontal]/segment-group:translate-y-px",
"group-data-[variant=underline]/segment-group:group-data-[orientation=vertical]/segment-group:w-0.5 group-data-[variant=underline]/segment-group:group-data-[orientation=vertical]/segment-group:left-[calc(var(--left)/2)] group-data-[variant=underline]/segment-group:group-data-[orientation=vertical]/segment-group:-translate-x-px",
)}
data-slot="segment-group-indicator"
/>
</div>
);
};
export const Segment = ({
className,
children,
...props
}: SegmentGroupPrimitive.ItemProps) => {
return (
<SegmentGroupPrimitive.Item
className={cn(
"relative flex min-h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] font-medium text-base outline-none transition-[color,background-color,box-shadow] hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-readonly:cursor-default data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start data-[state=checked]:text-foreground data-disabled:opacity-64 sm:h-8 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
className,
)}
data-slot="segment-group-item"
{...props}
>
{typeof children === "string" ? (
<SegmentGroupPrimitive.ItemText>
{children}
</SegmentGroupPrimitive.ItemText>
) : (
children
)}
<SegmentGroupPrimitive.ItemControl className={"hidden"} />
<SegmentGroupPrimitive.ItemHiddenInput />
</SegmentGroupPrimitive.Item>
);
};
export const SegmentGroupRootProvider = ({
children,
className,
variant = "default",
...props
}: SegmentGroupPrimitive.RootProviderProps & { variant?: TabsVariant }) => {
return (
<SegmentGroupPrimitive.RootProvider
className={cn("group/segment-group", className)}
data-variant={variant}
{...props}
>
{children}
</SegmentGroupPrimitive.RootProvider>
);
};
export const SegmentGroupContext = ({
...props
}: SegmentGroupPrimitive.ContextProps) => {
return <SegmentGroupPrimitive.Context {...props} />;
};
Update import aliases to match your project setup.
Usage
import * as SegmentGroup from "@/components/ui/segment-group"Read exported parts in src/components/ui/segment-group.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Underline tabs
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupUnderlineTabsDemo = () => (
<SegmentGroupRoot
className="max-w-xl border-b"
defaultValue="react"
variant="underline"
>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="svelte">Svelte</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupUnderlineTabsDemo;
Underline vertical tabs
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupUnderlineVerticalTabsDemo = () => (
<SegmentGroupRoot
className="max-w-xl border-s"
defaultValue="react"
orientation="vertical"
variant="underline"
>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="svelte">Svelte</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupUnderlineVerticalTabsDemo;
Default
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupDefaultDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react" variant="default">
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupDefaultDemo;
Controlled
Value: react
import { useState } from "react";
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupControlledDemo = () => {
const [value, setValue] = useState<string | null>("react");
return (
<div className="flex flex-col gap-2">
<p className="text-muted-foreground text-xs">
Value: <span className="font-medium text-foreground">{value}</span>
</p>
<SegmentGroupRoot
value={value}
onValueChange={({ value }) => setValue(value)}
>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
</div>
);
};
export default SegmentGroupControlledDemo;
Root provider
import { Button } from "@/components/ui/button";
import {
Segment,
SegmentGroup,
SegmentGroupRootProvider,
useSegmentGroup,
} from "@/components/ui/segment-group";
const SegmentGroupRootProviderDemo = () => {
const store = useSegmentGroup({ defaultValue: "solid" });
return (
<div className="flex flex-col gap-3">
<SegmentGroupRootProvider value={store}>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRootProvider>
<Button
onClick={() => {
store.setValue("vue");
}}
type="button"
variant="outline"
>
Set to Vue
</Button>
</div>
);
};
export default SegmentGroupRootProviderDemo;
Disabled group
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupDisabledGroupDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react" disabled>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupDisabledGroupDemo;
Disabled item
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupDisabledItemDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react">
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment disabled value="solid">
Solid (disabled)
</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupDisabledItemDemo;
Orientation
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupOrientationDemo = () => (
<div className="flex max-w-xs flex-col gap-3">
<SegmentGroupRoot defaultValue="a" orientation="vertical" variant="default">
<SegmentGroup>
<Segment value="a">Alpha</Segment>
<Segment value="b">Beta</Segment>
<Segment value="c">Gamma</Segment>
</SegmentGroup>
</SegmentGroupRoot>
</div>
);
export default SegmentGroupOrientationDemo;
Label
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupWithLabelDemo = () => (
<SegmentGroupRoot className="w-full max-w-md" defaultValue="next">
<SegmentGroup>
<Segment value="next">Next.js</Segment>
<Segment value="vite">Vite</Segment>
<Segment value="astro">Astro</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupWithLabelDemo;
Form
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupFormDemo = () => {
const [submitted, setSubmitted] = useState<string | null>(null);
return (
<form
className="flex flex-col gap-3"
onSubmit={(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
setSubmitted(String(formData.get("framework") ?? ""));
}}
>
<SegmentGroupRoot
className="max-w-md"
defaultValue="vite"
name="framework"
>
<SegmentGroup>
<Segment value="next">Next.js</Segment>
<Segment value="vite">Vite</Segment>
<Segment value="astro">Astro</Segment>
</SegmentGroup>
</SegmentGroupRoot>
<Button type="submit">Submit</Button>
{submitted ? (
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="font-medium text-foreground">{submitted}</span>
</p>
) : null}
</form>
);
};
export default SegmentGroupFormDemo;
Read-only
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupReadOnlyDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="solid" readOnly>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupReadOnlyDemo;
Invalid
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupInvalidDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react" invalid>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupInvalidDemo;
With field
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupWithFieldDemo = () => (
<Field className="max-w-md gap-3">
<FieldLabel>View</FieldLabel>
<SegmentGroupRoot defaultValue="month" name="view-field">
<SegmentGroup>
<Segment value="month">Month</Segment>
<Segment value="week">Week</Segment>
<Segment value="day">Day</Segment>
</SegmentGroup>
</SegmentGroupRoot>
<FieldDescription>
Pick how dates are shown in the calendar.
</FieldDescription>
</Field>
);
export default SegmentGroupWithFieldDemo;
useSegmentGroupContext hook
useSegmentGroupContext: b
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
useSegmentGroupContext,
} from "@/components/ui/segment-group";
const Inner = () => {
const context = useSegmentGroupContext();
return (
<p className="text-muted-foreground text-xs">
useSegmentGroupContext:{" "}
<span className="font-medium text-foreground">{context.value}</span>
</p>
);
};
const SegmentGroupContextHookDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="b">
<Inner />
<SegmentGroup>
<Segment value="a">A</Segment>
<Segment value="b">B</Segment>
<Segment value="c">C</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupContextHookDemo;
SegmentGroupContext render prop
SegmentGroupContext: react
import {
Segment,
SegmentGroup,
SegmentGroupContext,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupContextRenderDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react">
<SegmentGroupContext>
{(context) => (
<p className="text-muted-foreground text-xs">
SegmentGroupContext:{" "}
<span className="font-medium text-foreground">{context.value}</span>
</p>
)}
</SegmentGroupContext>
<SegmentGroup>
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupContextRenderDemo;
Rich item text
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupWithDescriptionDemo = () => (
<SegmentGroupRoot className="max-w-lg" defaultValue="free">
<SegmentGroup>
<Segment value="free">
<span className="flex flex-col gap-0.5">
<span className="font-medium">Free</span>
<span className="font-normal text-[0.7rem] text-muted-foreground leading-snug">
Basic features for personal use.
</span>
</span>
</Segment>
<Segment value="pro">
<span className="flex flex-col gap-0.5">
<span className="font-medium">Pro</span>
<span className="font-normal text-[0.7rem] text-muted-foreground leading-snug">
Advanced tools for teams.
</span>
</span>
</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupWithDescriptionDemo;
Items shortcut
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupItemsShortcutDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="react">
<SegmentGroup className="max-w-md">
<Segment value="react">React</Segment>
<Segment value="solid">Solid</Segment>
<Segment value="svelte">Svelte</Segment>
<Segment value="vue">Vue</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupItemsShortcutDemo;
Anatomy
import {
Segment,
SegmentGroup,
SegmentGroupRoot,
} from "@/components/ui/segment-group";
const SegmentGroupAnatomyDemo = () => (
<SegmentGroupRoot className="max-w-md" defaultValue="1">
<SegmentGroup>
<Segment value="1">One</Segment>
<Segment value="2">Two</Segment>
</SegmentGroup>
</SegmentGroupRoot>
);
export default SegmentGroupAnatomyDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
SegmentGroupRoot
| Prop | Type | Description |
|---|---|---|
| variant? | "default" | "underline" | Chrome variant for items and indicator. |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.