moduix-react
itemangle-slidersource: ok · hit
AngleSlider
moduix React AngleSlider component.
npx shadcn add "https://moduix.dev/r/react/angle-slider.json"free
Interaction
carousel
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@ark-ui/react
Registry dependencies
@moduix-react/foundation
Files3 · source cached 10/15/2026, 3:42:30 AM
src/components/angle-slider/AngleSlider.tsx
'use client';
import {
AngleSlider as AngleSliderPrimitive,
useAngleSlider as useAngleSliderPrimitive,
useAngleSliderContext,
} from '@ark-ui/react/angle-slider';
import { clsx } from 'clsx';
import type { ComponentProps, ComponentRef, ReactNode } from 'react';
import { forwardRef } from 'react';
import styles from './AngleSlider.module.css';
const AngleSliderRoot = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Root>,
ComponentProps<typeof AngleSliderPrimitive.Root>
>(function AngleSliderRoot({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.Root
ref={ref}
className={clsx(styles.root, className)}
{...props}
data-slot="angle-slider-root"
/>
);
});
const AngleSliderLabel = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Label>,
ComponentProps<typeof AngleSliderPrimitive.Label>
>(function AngleSliderLabel({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.Label
ref={ref}
className={clsx(styles.label, className)}
{...props}
data-slot="angle-slider-label"
/>
);
});
const AngleSliderRootProvider = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.RootProvider>,
ComponentProps<typeof AngleSliderPrimitive.RootProvider>
>(function AngleSliderRootProvider({ className, value, ...props }, ref) {
return (
<AngleSliderPrimitive.RootProvider
ref={ref}
className={clsx(styles.root, className)}
value={value}
{...props}
data-slot="angle-slider-root-provider"
/>
);
});
const AngleSliderControl = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Control>,
ComponentProps<typeof AngleSliderPrimitive.Control>
>(function AngleSliderControl({ className, onPointerDown, ...props }, ref) {
return (
<AngleSliderPrimitive.Control
ref={ref}
className={clsx(styles.control, className)}
{...props}
onPointerDown={(event) => {
onPointerDown?.(event);
if (event.defaultPrevented || event.button !== 0) return;
if (event.currentTarget.matches('[data-disabled], [data-readonly]')) return;
event.preventDefault();
event.currentTarget
.querySelector<HTMLElement>('[data-scope="angle-slider"][data-part="thumb"]')
?.focus({ preventScroll: true, focusVisible: false });
}}
data-slot="angle-slider-control"
/>
);
});
const AngleSliderThumb = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Thumb>,
ComponentProps<typeof AngleSliderPrimitive.Thumb>
>(function AngleSliderThumb({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.Thumb
ref={ref}
className={clsx(styles.thumb, className)}
{...props}
data-slot="angle-slider-thumb"
/>
);
});
const AngleSliderMarkerGroup = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.MarkerGroup>,
ComponentProps<typeof AngleSliderPrimitive.MarkerGroup>
>(function AngleSliderMarkerGroup({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.MarkerGroup
ref={ref}
className={clsx(styles.markerGroup, className)}
{...props}
data-slot="angle-slider-marker-group"
/>
);
});
const AngleSliderMarker = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Marker>,
ComponentProps<typeof AngleSliderPrimitive.Marker>
>(function AngleSliderMarker({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.Marker
ref={ref}
className={clsx(styles.marker, className)}
{...props}
data-slot="angle-slider-marker"
/>
);
});
type AngleSliderMarksProps = Omit<
ComponentProps<typeof AngleSliderPrimitive.MarkerGroup>,
'asChild' | 'children'
> & {
values: readonly number[];
};
const AngleSliderMarks = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.MarkerGroup>,
AngleSliderMarksProps
>(function AngleSliderMarks({ values, ...props }, ref) {
return (
<AngleSliderMarkerGroup ref={ref} {...props}>
{values.map((value, index) => (
<AngleSliderMarker key={`${value}-${index}`} value={value} />
))}
</AngleSliderMarkerGroup>
);
});
type AngleSliderDialProps = Omit<
ComponentProps<typeof AngleSliderPrimitive.Control>,
'asChild' | 'children'
> & {
children?: ReactNode;
};
const AngleSliderDial = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.Control>,
AngleSliderDialProps
>(function AngleSliderDial({ children, ...props }, ref) {
return (
<AngleSliderControl ref={ref} {...props}>
{children}
<AngleSliderValueText />
<AngleSliderThumb />
</AngleSliderControl>
);
});
const AngleSliderValueText = forwardRef<
ComponentRef<typeof AngleSliderPrimitive.ValueText>,
ComponentProps<typeof AngleSliderPrimitive.ValueText>
>(function AngleSliderValueText({ className, ...props }, ref) {
return (
<AngleSliderPrimitive.ValueText
ref={ref}
className={clsx(styles.valueText, className)}
{...props}
data-slot="angle-slider-value-text"
/>
);
});
const AngleSlider = AngleSliderRoot;
const AngleSliderContext = AngleSliderPrimitive.Context;
const AngleSliderHiddenInput = AngleSliderPrimitive.HiddenInput;
const useAngleSlider = useAngleSliderPrimitive;
export {
AngleSlider,
AngleSliderContext,
AngleSliderControl,
AngleSliderDial,
AngleSliderHiddenInput,
AngleSliderLabel,
AngleSliderMarker,
AngleSliderMarkerGroup,
AngleSliderMarks,
AngleSliderRootProvider,
AngleSliderThumb,
AngleSliderValueText,
useAngleSlider,
useAngleSliderContext,
};src/components/angle-slider/AngleSlider.module.css
@layer moduix.components {
.root {
box-sizing: border-box;
display: inline-flex;
flex-direction: column;
min-width: 0;
align-items: center;
gap: var(--moduix-angle-slider-gap, var(--moduix-spacing-3));
color: var(--moduix-angle-slider-color, var(--moduix-color-foreground));
&[data-disabled] {
opacity: var(--moduix-angle-slider-disabled-opacity, var(--moduix-opacity-disabled));
}
}
.label {
color: var(
--moduix-angle-slider-label-color,
var(--moduix-angle-slider-color, var(--moduix-color-foreground))
);
font-size: var(--moduix-angle-slider-label-font-size, var(--moduix-text-sm));
font-weight: var(--moduix-angle-slider-label-font-weight, var(--moduix-weight-medium));
line-height: var(--moduix-angle-slider-label-line-height, var(--moduix-line-height-text-sm));
text-align: center;
}
.control {
--_angle-slider-fill-color: var(
--moduix-angle-slider-indicator-bg,
var(--moduix-color-primary)
);
--_angle-slider-track-color: var(--moduix-angle-slider-track-bg, var(--moduix-color-muted));
--_angle-slider-thumb-border-color: var(
--moduix-angle-slider-thumb-border-color,
var(--moduix-color-border)
);
position: relative;
box-sizing: border-box;
width: var(--moduix-angle-slider-size, 8rem);
min-width: 0;
aspect-ratio: 1;
border-radius: var(--moduix-radius-full);
cursor: pointer;
outline: 0;
user-select: none;
&::before {
position: absolute;
inset: 0;
border-radius: inherit;
background: conic-gradient(
from 0deg,
var(--_angle-slider-fill-color) var(--angle, 0deg),
var(--_angle-slider-track-color) var(--angle, 0deg)
);
mask: radial-gradient(
closest-side,
transparent calc(100% - var(--moduix-angle-slider-ring-thickness, 0.5rem) - 1px),
#000 calc(100% - var(--moduix-angle-slider-ring-thickness, 0.5rem))
);
content: '';
}
&::after {
position: absolute;
top: calc(var(--moduix-angle-slider-ring-thickness, 0.5rem) / 2);
left: 50%;
z-index: 1;
width: var(--moduix-angle-slider-ring-thickness, 0.5rem);
height: var(--moduix-angle-slider-ring-thickness, 0.5rem);
border-radius: var(--moduix-radius-full);
background: var(--_angle-slider-fill-color);
transform: translate(-50%, -50%);
content: '';
}
@media (hover: hover) {
&:not([data-disabled], [data-readonly]):hover::before {
--_angle-slider-track-color: var(
--moduix-angle-slider-track-bg-hover,
var(--moduix-angle-slider-track-bg, var(--moduix-color-muted))
);
}
}
&:not([data-disabled], [data-readonly]):active::before {
--_angle-slider-track-color: var(
--moduix-angle-slider-track-bg-active,
var(--moduix-angle-slider-track-bg, var(--moduix-color-muted))
);
}
&:active:not([data-disabled], [data-readonly]) .thumb::before {
border-color: var(--moduix-angle-slider-focus-ring-color, var(--moduix-color-ring));
box-shadow:
0 0 0 var(--moduix-focus-ring-inset-width, var(--moduix-border-width-sm))
var(--moduix-angle-slider-focus-ring-color, var(--moduix-color-ring)),
var(--moduix-angle-slider-thumb-shadow-dragging, var(--moduix-shadow-md));
}
&[data-invalid] {
--_angle-slider-fill-color: var(
--moduix-angle-slider-invalid-indicator-bg,
var(--moduix-angle-slider-invalid-color, var(--moduix-color-destructive))
);
--_angle-slider-thumb-border-color: var(
--moduix-angle-slider-invalid-indicator-bg,
var(--moduix-angle-slider-invalid-color, var(--moduix-color-destructive))
);
}
&:is([data-disabled], [data-readonly]) {
cursor: default;
}
}
.thumb {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
z-index: 2;
width: 0;
outline: 0;
&::before {
position: absolute;
top: calc(var(--moduix-angle-slider-ring-thickness, 0.5rem) / 2);
left: 0;
box-sizing: border-box;
width: var(--moduix-angle-slider-thumb-size, var(--moduix-spacing-4));
height: var(--moduix-angle-slider-thumb-size, var(--moduix-spacing-4));
border: var(--moduix-angle-slider-thumb-border-width, var(--moduix-border-width-sm)) solid
var(--_angle-slider-thumb-border-color);
border-radius: var(--moduix-angle-slider-thumb-radius, var(--moduix-radius-full));
background: var(--moduix-angle-slider-thumb-bg, var(--moduix-color-background));
box-shadow: var(--moduix-angle-slider-thumb-shadow, var(--moduix-shadow-sm));
transform: translate(-50%, -50%);
transition:
border-color var(--moduix-angle-slider-transition, var(--moduix-transition-default)),
background-color var(--moduix-angle-slider-transition, var(--moduix-transition-default)),
box-shadow var(--moduix-angle-slider-transition, var(--moduix-transition-default));
content: '';
}
&:focus-visible::before {
border-color: var(--moduix-angle-slider-focus-ring-color, var(--moduix-color-ring));
box-shadow: 0 0 0 var(--moduix-focus-ring-inset-width, var(--moduix-border-width-sm))
var(--moduix-angle-slider-focus-ring-color, var(--moduix-color-ring));
}
&[data-disabled] {
pointer-events: none;
}
}
.markerGroup {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.marker {
position: absolute;
inset: 0;
&::before {
position: absolute;
top: calc(var(--moduix-angle-slider-ring-thickness, 0.5rem) / 2);
left: 50%;
width: var(--moduix-angle-slider-marker-width, 0.125rem);
height: var(--moduix-angle-slider-marker-height, 0.5rem);
border-radius: var(--moduix-radius-full);
background: var(--moduix-angle-slider-marker-color, var(--moduix-color-muted-foreground));
transform: translate(-50%, -50%);
content: '';
}
&[data-state='under-value']::before {
background: var(--moduix-angle-slider-marker-active-color, var(--moduix-color-primary));
}
&[data-state='at-value']::before {
background: var(--moduix-angle-slider-marker-current-color, var(--moduix-color-foreground));
}
}
.valueText {
color: var(
--moduix-angle-slider-value-text-color,
var(--moduix-angle-slider-color, var(--moduix-color-foreground))
);
font-size: var(--moduix-angle-slider-value-text-font-size, var(--moduix-text-lg));
font-weight: var(--moduix-angle-slider-value-text-font-weight, var(--moduix-weight-medium));
font-variant-numeric: tabular-nums;
line-height: var(
--moduix-angle-slider-value-text-line-height,
var(--moduix-line-height-text-lg)
);
text-align: center;
}
.control .valueText {
position: absolute;
inset: 0;
z-index: 1;
display: grid;
place-items: center;
}
@media (prefers-reduced-motion: reduce) {
.thumb::before {
transition: none;
}
}
}src/components/angle-slider/index.ts
export * from './AngleSlider.js';