shadimage
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 · light
ui-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';

More from moduix-react

Browse all components in this library →