Skip to content

feat: Add SliderFill component and improve default SliderOutput formatting#10021

Open
devongovett wants to merge 5 commits intomainfrom
slider-fill
Open

feat: Add SliderFill component and improve default SliderOutput formatting#10021
devongovett wants to merge 5 commits intomainfrom
slider-fill

Conversation

@devongovett
Copy link
Copy Markdown
Member

This improves the RAC Slider API to make it easier to render a slider with a fill. Previously this had to be implemented manually using the render props. There is some complexity to this to handle horizontal and vertical orientation, fill offset, etc. This PR adds a <SliderFill> component with some default styles that handle these calculations for you. When there is a single SliderThumb, the fill is rendered from the start (or a custom offset if one is provided). When there are multiple thumbs, the fill is shown between the first and last.

Also updated the default behavior for the <SliderOutput> component to handle formatting for ranges (when there are two thumbs), and lists when there are three or more.

@rspbot
Copy link
Copy Markdown

rspbot commented May 6, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 6, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 6, 2026

## API Changes

react-aria-components

/react-aria-components:ColorSliderState

 ColorSliderState {
   decrementThumb: (number, number) => void
   defaultValues: Array<number>
   focusedThumb: number | undefined
   getDisplayColor: () => Color
-  getFormattedValue: (number) => string
+  getFormattedValue: (number | Array<number>) => string
   getPercentValue: (number) => number
   getThumbMaxValue: (number) => number
   getThumbMinValue: (number) => number
   getThumbPercent: (number) => number
   getThumbValueLabel: (number) => string
   getValuePercent: (number) => number
   incrementThumb: (number, number) => void
   isDisabled: boolean
   isDragging: boolean
   isThumbDragging: (number) => boolean
   isThumbEditable: (number) => boolean
   orientation: Orientation
   pageSize: number
   setFocusedThumb: (number | undefined) => void
   setThumbDragging: (number, boolean) => void
   setThumbEditable: (number, boolean) => void
   setThumbPercent: (number, number) => void
   setThumbValue: (number, number) => void
   setValue: (string | Color) => void
   step: number
   value: Color
   values: Array<number>
 }

/react-aria-components:SliderState

 SliderState {
   decrementThumb: (number, number) => void
   defaultValues: Array<number>
   focusedThumb: number | undefined
-  getFormattedValue: (number) => string
+  getFormattedValue: (number | Array<number>) => string
   getPercentValue: (number) => number
   getThumbMaxValue: (number) => number
   getThumbMinValue: (number) => number
   getThumbPercent: (number) => number
   getThumbValueLabel: (number) => string
   getValuePercent: (number) => number
   incrementThumb: (number, number) => void
   isDisabled: boolean
   isThumbDragging: (number) => boolean
   isThumbEditable: (number) => boolean
   orientation: Orientation
   pageSize: number
   setFocusedThumb: (number | undefined) => void
   setThumbDragging: (number, boolean) => void
   setThumbEditable: (number, boolean) => void
   setThumbPercent: (number, number) => void
   setThumbValue: (number, number) => void
   step: number
   values: Array<number>
 }

/react-aria-components:SliderFill

+SliderFill {
+  children?: ChildrenOrFunction<SliderFillRenderProps>
+  className?: ClassNameOrFunction<SliderFillRenderProps> = 'react-aria-SliderFill'
+  offset?: number = 0
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SliderFillRenderProps>
+  style?: StyleOrFunction<SliderFillRenderProps>
+}

/react-aria-components:SliderFillContext

+SliderFillContext {
+  UNTYPED
+}

/react-aria-components:SliderFillProps

+SliderFillProps {
+  children?: ChildrenOrFunction<SliderFillRenderProps>
+  className?: ClassNameOrFunction<SliderFillRenderProps> = 'react-aria-SliderFill'
+  offset?: number = 0
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SliderFillRenderProps>
+  style?: StyleOrFunction<SliderFillRenderProps>
+}

/react-aria-components:SliderFillRenderProps

+SliderFillRenderProps {
+  isDisabled: boolean
+  isHovered: boolean
+  orientation: Orientation
+  state: SliderState
+}

@react-stately/color

/@react-stately/color:ColorSliderState

 ColorSliderState {
   decrementThumb: (number, number) => void
   defaultValues: Array<number>
   focusedThumb: number | undefined
   getDisplayColor: () => Color
-  getFormattedValue: (number) => string
+  getFormattedValue: (number | Array<number>) => string
   getPercentValue: (number) => number
   getThumbMaxValue: (number) => number
   getThumbMinValue: (number) => number
   getThumbPercent: (number) => number
   getThumbValueLabel: (number) => string
   getValuePercent: (number) => number
   incrementThumb: (number, number) => void
   isDisabled: boolean
   isDragging: boolean
   isThumbDragging: (number) => boolean
   isThumbEditable: (number) => boolean
   orientation: Orientation
   pageSize: number
   setFocusedThumb: (number | undefined) => void
   setThumbDragging: (number, boolean) => void
   setThumbEditable: (number, boolean) => void
   setThumbPercent: (number, number) => void
   setThumbValue: (number, number) => void
   setValue: (string | Color) => void
   step: number
   value: Color
   values: Array<number>
 }

@react-stately/slider

/@react-stately/slider:SliderState

 SliderState {
   decrementThumb: (number, number) => void
   defaultValues: Array<number>
   focusedThumb: number | undefined
-  getFormattedValue: (number) => string
+  getFormattedValue: (number | Array<number>) => string
   getPercentValue: (number) => number
   getThumbMaxValue: (number) => number
   getThumbMinValue: (number) => number
   getThumbPercent: (number) => number
   getThumbValueLabel: (number) => string
   getValuePercent: (number) => number
   incrementThumb: (number, number) => void
   isDisabled: boolean
   isThumbDragging: (number) => boolean
   isThumbEditable: (number) => boolean
   orientation: Orientation
   pageSize: number
   setFocusedThumb: (number | undefined) => void
   setThumbDragging: (number, boolean) => void
   setThumbEditable: (number, boolean) => void
   setThumbPercent: (number, number) => void
   setThumbValue: (number, number) => void
   step: number
   values: Array<number>
 }

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Slider authoring experience in React Aria Components by introducing a dedicated <SliderFill> component for rendering the selected range, and by updating <SliderOutput>’s default formatting to better handle multi-thumb sliders (range + list formatting). It also updates starter implementations, Spectrum S2 usage, docs, and tests to adopt the new APIs.

Changes:

  • Added a new SliderFill component (and exports) to render the selected range with built-in orientation/offset handling.
  • Updated default SliderOutput rendering to use state.getFormattedValue() with improved formatting for 1, 2, and 3+ thumb values.
  • Refactored starter sliders, Spectrum S2 sliders, docs, and tests to use SliderFill and the new default output behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
starters/tailwind/src/Slider.tsx Replaces manual fill sizing with <SliderFill> and uses default <SliderOutput> formatting; adds fillOffset wrapper prop.
starters/docs/src/Slider.tsx Updates docs starter slider to use <SliderFill> and default <SliderOutput>; adds fillOffset wrapper prop.
starters/docs/src/Slider.css Migrates styling hooks from .fill to .react-aria-SliderFill to match the new component.
packages/react-stately/src/slider/useSliderState.ts Enhances getFormattedValue to support single values, ranges, and lists (via formatRange and Intl.ListFormat).
packages/react-stately/src/color/useColorSliderState.ts Supplies a real numberFormatter to useSliderState and overrides getFormattedValue for color channel formatting.
packages/react-aria-components/test/Slider.test.js Updates output expectations and adds coverage for SliderFill rendering/style behavior.
packages/react-aria-components/src/Slider.tsx Adds SliderFill component, context, and updates SliderOutput default content to state.getFormattedValue().
packages/react-aria-components/exports/Slider.ts Exports SliderFill/SliderFillContext and related types.
packages/react-aria-components/exports/index.ts Re-exports SliderFill and its types from the package entry.
packages/dev/s2-docs/pages/react-aria/Slider.mdx Updates docs examples/anatomy/prop tables to include SliderFill and starter fillOffset.
packages/@react-spectrum/s2/src/Slider.tsx Replaces custom filled track logic with <SliderFill> and adopts new output formatting.
packages/@react-spectrum/s2/src/RangeSlider.tsx Replaces custom filled track logic with <SliderFill> for range sliders.

thumbLabels?: string[];
/**
* The offset from which to start the fill.
* @default 0
thumbLabels?: string[];
/**
* The offset from which to start the fill.
* @default 0
export interface SliderFillProps extends HoverEvents, RenderProps<SliderFillRenderProps>, GlobalDOMAttributes<HTMLDivElement> {
/**
* The offset from which to start the fill.
* @default 0
Comment on lines +365 to +377
defaultStyle: state.orientation === 'vertical'
? {
position: 'absolute',
bottom: `${startPercent}%`,
height: `${sizePercent}%`,
width: '100%'
}
: {
position: 'absolute',
insetInlineStart: `${startPercent}%`,
width: `${sizePercent}%`,
height: '100%'
},
let {onHoverStart, onHoverEnd, onHoverChange, ...otherProps} = props;
let {hoverProps, isHovered} = useHover({onHoverStart, onHoverEnd, onHoverChange});

let offset = props.offset != null ? clamp(props.offset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : state.getThumbMinValue(0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on multiple SliderFills that allow you to specify start and end thumbs?

@snowystinger
Copy link
Copy Markdown
Member

Half related, do we want a similar component for ProgressBar and Meter? I know those might be more complicated because they could be round and sliders are more likely to be a straight bar. Not in this PR, just a general thought.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants