diff --git a/README.md b/README.md
index 223b759..ef4b118 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Below is a list of the components available in this library. Each component has
- [BBBScrollArea](./src/components/ScrollArea/README.md)
- [BBBSearch](./src/components/Search/README.md)
- [BBBSelect](./src/components/Select/README.md)
+- [BBBSlider](./src/components/Slider/README.md)
- [BBBSpinner](./src/components/Spinner//README.md)
- [BBBTextAreaInput](./src/components/TextAreaInput/README.md)
- [BBBTextInput](./src/components/TextInput/README.md)
diff --git a/package.json b/package.json
index bea56d7..6d53f7a 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,13 @@
"require": "./dist/components/Select.js",
"default": "./dist/components/Select.js"
},
+ "./Slider": {
+ "types": "./dist/types/components/Slider/index.d.ts",
+ "node": "./dist/components/Slider.js",
+ "import": "./dist/esm/components/Slider/index.js",
+ "require": "./dist/components/Slider.js",
+ "default": "./dist/components/Slider.js"
+ },
"./Spinner": {
"types": "./dist/types/components/Spinner/index.d.ts",
"node": "./dist/components/Spinner.js",
@@ -211,6 +218,9 @@
"Select": [
"dist/types/components/Select/index.d.ts"
],
+ "Slider": [
+ "dist/types/components/Slider/index.d.ts"
+ ],
"Spinner": [
"dist/types/components/Spinner/index.d.ts"
],
diff --git a/src/components/Slider/README.md b/src/components/Slider/README.md
new file mode 100644
index 0000000..ae036d9
--- /dev/null
+++ b/src/components/Slider/README.md
@@ -0,0 +1,137 @@
+# BBBSlider
+
+The `BBBSlider` component provides a horizontal, single-thumb slider for picking a value inside a
+range. It supports labels (`label` and `helperText`), decorative end icons, marks with or without
+tick dots, and a value tooltip.
+
+
+
+## Usage Example
+
+### Slider with a label
+
+```jsx
+import { BBBSlider } from 'bbb-ui-components-react';
+
+
+```
+
+### Volume, where the number matters
+
+```jsx
+import { BBBSlider } from 'bbb-ui-components-react';
+
+ `${value}%`}
+ getAriaValueText={(value) => `${value} percent`}
+ dataTest="volume-slider"
+ onChange={(event, value) => setVolume(value)}
+/>
+```
+
+### Brightness, where the number does not matter
+
+```jsx
+import { BBBSlider } from 'bbb-ui-components-react';
+import { MdBrightnessLow, MdBrightnessHigh } from 'react-icons/md';
+
+}
+ iconEnd={}
+ valueLabelDisplay="off"
+ onChange={(event, value) => setBrightness(value)}
+/>
+```
+
+### Discrete stages with labels only
+
+```jsx
+import { BBBSlider } from 'bbb-ui-components-react';
+
+ setGridSize(value)}
+/>
+```
+
+## Accessibility
+
+- Give every slider an accessible name: either `label` (which also renders visible text and is
+ wired to the input with `htmlFor`) or `ariaLabel` when the design has no visible label.
+- `helperText` is referenced by the input's `aria-describedby` automatically; `ariaDescribedBy`
+ overrides it when the description lives elsewhere on the page.
+- When `valueLabelFormat` shows something other than the raw value, pass the matching
+ `getAriaValueText` so screen readers announce what the user sees.
+- Keyboard support comes from MUI: arrows move by `step`, Page Up/Page Down jump, Home/End go to
+ the ends.
+
+## Testing
+
+`dataTest` lands on the root element, and the focusable range input gets the same value suffixed
+with `-input`. Drive the input, not the root:
+
+```js
+await page.locator('[data-test="volume-slider-input"]').fill('80');
+```
+
+## Notes
+
+The value tooltip is rendered inside the slider, so a container with `overflow: hidden` can clip it
+when the slider sits at the very top of the container. Use `valueLabelDisplay="off"` or reserve
+vertical space above the slider in that case.
+
+Mark labels are centred on their tick, so the labels at `min` and `max` extend half their width past
+each end of the track. Reserve horizontal padding around the slider, or avoid labelling the extreme
+marks, when the container clips.
+
+`iconStart`/`iconEnd` render at `1.5rem` by default, matching a circle `BBButton`; pass a `size` on
+the icon itself to override it.
+
+## Props
+
+| Property | Type | Default | Description |
+| ------------------- | ------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| `label` | `string` | | The main label text displayed above the slider. |
+| `helperText` | `string` | | The helper text displayed below the label. |
+| `value` | `number` | | The current value; providing it makes the slider controlled. |
+| `defaultValue` | `number` | | The initial value used while the slider is uncontrolled. |
+| `min` | `number` | `0` | The minimum allowed value. |
+| `max` | `number` | `100` | The maximum allowed value. |
+| `step` | `number \| null` | `1` | The granularity the value moves by; `null` snaps to `marks`. |
+| `marks` | `boolean \| { value: number, label?: ReactNode }[]` | `false` | The marks rendered on the track. |
+| `showMarkTicks` | `boolean` | `true` | If `false`, renders the mark labels without their tick dots. |
+| `animate` | `boolean` | `true` | If `false`, disables transitions and hover/drag visual effects so state changes render instantly. |
+| `valueLabelDisplay` | `'auto' \| 'on' \| 'off'` | `'auto'` | When the value tooltip is shown. |
+| `valueLabelFormat` | `string \| ((value: number, index: number) => ReactNode)` | | Formats the value shown in the tooltip. |
+| `getAriaValueText` | `(value: number, index: number) => string` | | The text screen readers announce for the current value. |
+| `iconStart` | `React.ReactNode` | | A decorative node rendered at the minimum end; it is hidden from screen readers. |
+| `iconEnd` | `React.ReactNode` | | A decorative node rendered at the maximum end; it is hidden from screen readers. |
+| `disabled` | `boolean` | `false` | If `true`, the slider is disabled and unresponsive. |
+| `onChange` | `(event: Event, value: number) => void` | | Fired continuously while the user drags or presses a key. |
+| `onChangeCommitted` | `(event: React.SyntheticEvent \| Event, value: number) => void` | | Fired once, when the interaction ends; prefer it for expensive updates. |
+| `id` | `string` | | The HTML `id` of the underlying range input; auto-generated when omitted. |
+| `dataTest` | `string` | | The `data-test` attribute for the root; the range input gets `${dataTest}-input`. |
+| `ariaLabel` | `string` | | The accessible name for the slider. |
+| `ariaLabelledBy` | `string` | | The ID of the element that labels the slider; ignored when `ariaLabel` is set, and falls back to the rendered label's id when omitted. |
+| `ariaDescribedBy` | `string` | | The ID of the element that describes the slider; falls back to the rendered helper text's id when omitted. |
+| `...props` | `MuiSliderProps` | | Any other props are passed down to the underlying Material-UI Slider component. |
diff --git a/src/components/Slider/assets/example.png b/src/components/Slider/assets/example.png
new file mode 100644
index 0000000..b606678
Binary files /dev/null and b/src/components/Slider/assets/example.png differ
diff --git a/src/components/Slider/component.stories.tsx b/src/components/Slider/component.stories.tsx
new file mode 100644
index 0000000..c329062
--- /dev/null
+++ b/src/components/Slider/component.stories.tsx
@@ -0,0 +1,232 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { MdBrightnessLow, MdBrightnessHigh } from 'react-icons/md';
+import BBBSlider from './component';
+import { VALUE_LABEL_DISPLAY_VALUES, DEFAULT_VALUE_LABEL_DISPLAY } from './constants';
+
+const meta = {
+ title: 'BBBSlider',
+ component: BBBSlider,
+ tags: ['autodocs'],
+ argTypes: {
+ label: {
+ control: 'text',
+ description: 'Main label text displayed above the slider.',
+ },
+ helperText: {
+ control: 'text',
+ description: 'Helper text displayed below the label.',
+ },
+ min: {
+ control: 'number',
+ description: 'Minimum allowed value.',
+ },
+ max: {
+ control: 'number',
+ description: 'Maximum allowed value.',
+ },
+ step: {
+ control: 'number',
+ description: 'Granularity the value can move by; use null with marks to snap to them.',
+ },
+ marks: {
+ control: 'object',
+ description: 'Marks to render on the track; true spreads them by step, or pass an array with labels.',
+ },
+ showMarkTicks: {
+ control: 'boolean',
+ description: 'Renders the tick dots of `marks`; set to false to keep the mark labels only.',
+ table: { defaultValue: { summary: 'true' } },
+ },
+ animate: {
+ control: 'boolean',
+ description: 'Enables transitions and hover/drag visual effects; set to false for instant state changes.',
+ table: { defaultValue: { summary: 'true' } },
+ },
+ valueLabelDisplay: {
+ control: 'select',
+ options: VALUE_LABEL_DISPLAY_VALUES,
+ description: 'Controls when the value tooltip is shown.',
+ table: { defaultValue: { summary: `${DEFAULT_VALUE_LABEL_DISPLAY}` } },
+ },
+ iconStart: {
+ control: false,
+ description: 'Decorative node rendered at the minimum end of the track.',
+ },
+ iconEnd: {
+ control: false,
+ description: 'Decorative node rendered at the maximum end of the track.',
+ },
+ valueLabelFormat: {
+ control: false,
+ description: 'Formats the value shown in the tooltip; receives the raw value.',
+ },
+ getAriaValueText: {
+ control: false,
+ description: 'Returns the text screen readers announce for the current value.',
+ },
+ disabled: {
+ control: 'boolean',
+ description: 'Disable the slider when true.',
+ },
+ dataTest: {
+ control: 'text',
+ description: 'Value for the data-test attribute on the root; the range input gets `${dataTest}-input`.',
+ },
+ ariaLabel: {
+ control: 'text',
+ description: 'Accessible name for the slider.',
+ },
+ onChange: {
+ control: false,
+ description: 'Change handler fired continuously while the user drags or presses a key.',
+ },
+ onChangeCommitted: {
+ control: false,
+ description: 'Change handler fired once, when the interaction ends.',
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+/** Bare slider with no label, covering the default 0-100 range. */
+export const Default: Story = {
+ args: { defaultValue: 50, ariaLabel: 'Volume' },
+};
+
+/** Visible label and helper text; clicking the label focuses the slider. */
+export const WithLabel: Story = {
+ args: { label: 'Brightness', helperText: 'Applies to the whole image', defaultValue: 50 },
+};
+
+/** Disabled state, which keeps the label readable but blocks interaction. */
+export const Disabled: Story = {
+ args: { label: 'Brightness', defaultValue: 50, disabled: true },
+};
+
+/** Decorative icons at both ends, the brightness pattern where the number itself is irrelevant. */
+export const WithIcons: Story = {
+ args: {
+ label: 'Brightness',
+ iconStart: ,
+ iconEnd: ,
+ valueLabelDisplay: 'off',
+ min: -100,
+ max: 100,
+ defaultValue: 0,
+ ariaLabel: 'Camera brightness',
+ },
+};
+
+/** Discrete stages with labelled marks, snapping to each step. */
+export const WithMarks: Story = {
+ args: {
+ label: 'Webcam grid size',
+ min: 1,
+ max: 4,
+ step: 1,
+ marks: [
+ { value: 1, label: '1x1' },
+ { value: 2, label: '2x2' },
+ { value: 3, label: '3x3' },
+ { value: 4, label: '4x4' },
+ ],
+ defaultValue: 2,
+ valueLabelDisplay: 'off',
+ ariaLabel: 'Webcam grid size',
+ },
+ decorators: [
+ (StoryComponent) => (
+
+
+
+ ),
+ ],
+};
+
+/** Mark labels without the tick dots, reproducing the camera modal's -100/0/100 scale. */
+export const MarksWithoutTicks: Story = {
+ args: {
+ label: 'Brightness',
+ min: -100,
+ max: 100,
+ showMarkTicks: false,
+ marks: [
+ { value: -100, label: '-100' },
+ { value: 0, label: '0' },
+ { value: 100, label: '100' },
+ ],
+ defaultValue: 0,
+ ariaLabel: 'Camera brightness',
+ },
+ decorators: [
+ (StoryComponent) => (
+
+
+
+ ),
+ ],
+};
+
+/** Tooltip pinned open, for cases where the number must stay visible while adjusting. */
+export const ValueLabelAlwaysOn: Story = {
+ args: { label: 'Volume', defaultValue: 70, valueLabelDisplay: 'on', ariaLabel: 'Volume' },
+};
+
+/** Percentage formatting through valueLabelFormat, the volume pattern where the number matters. */
+export const PercentageFormat: Story = {
+ args: {
+ label: 'Volume',
+ defaultValue: 70,
+ valueLabelDisplay: 'on',
+ valueLabelFormat: (value: number) => `${value}%`,
+ getAriaValueText: (value: number) => `${value} percent`,
+ ariaLabel: 'Volume',
+ },
+};
+
+/** Stored value and displayed value differ: the range is 0-200 but the user sees -100 to 100. */
+export const OffsetFormat: Story = {
+ args: {
+ label: 'Brightness',
+ min: 0,
+ max: 200,
+ defaultValue: 100,
+ valueLabelDisplay: 'on',
+ valueLabelFormat: (value: number) => `${value - 100}`,
+ getAriaValueText: (value: number) => `${value - 100}`,
+ ariaLabel: 'Camera brightness',
+ },
+};
+
+/** Animation disabled: dragging the thumb and viewing the tooltip show no transitions or hover glow. */
+export const WithoutAnimation: Story = {
+ args: {
+ label: 'Volume',
+ defaultValue: 70,
+ valueLabelDisplay: 'on',
+ animate: false,
+ ariaLabel: 'Volume',
+ },
+};
+
+/** Same slider under a right-to-left ancestor: the track fills from the right. */
+export const RightToLeft: Story = {
+ args: {
+ label: 'Brightness',
+ min: -100,
+ max: 100,
+ defaultValue: 50,
+ valueLabelDisplay: 'on',
+ ariaLabel: 'Camera brightness',
+ },
+ decorators: [
+ (StoryComponent) => (
+
+
+
+ ),
+ ],
+};
diff --git a/src/components/Slider/component.tsx b/src/components/Slider/component.tsx
new file mode 100644
index 0000000..6f21302
--- /dev/null
+++ b/src/components/Slider/component.tsx
@@ -0,0 +1,110 @@
+import React, { JSX, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles';
+import { SliderProps } from './types';
+import * as Styled from './styles';
+import { DEFAULT_ANIMATE, DEFAULT_SHOW_MARK_TICKS, DEFAULT_VALUE_LABEL_DISPLAY } from './constants';
+
+/**
+ * A horizontal, single-thumb slider for picking a value inside a range.
+ *
+ * It supports labels (label and helperText), decorative end icons, marks and a value tooltip,
+ * and keeps its accessible name and description on the underlying range input.
+ *
+ */
+function Slider ({
+ label,
+ helperText,
+ iconStart,
+ iconEnd,
+ showMarkTicks = DEFAULT_SHOW_MARK_TICKS,
+ animate = DEFAULT_ANIMATE,
+ valueLabelDisplay = DEFAULT_VALUE_LABEL_DISPLAY,
+ onChange,
+ onChangeCommitted,
+ id: providedId,
+ dataTest,
+ ariaLabel,
+ ariaLabelledBy,
+ ariaDescribedBy,
+ slotProps,
+ ...rest
+}: SliderProps): JSX.Element {
+ const generatedUniqueId = useId();
+ const id = providedId ?? generatedUniqueId;
+ const labelId = label ? `${id}-label` : undefined;
+ const helperTextId = helperText ? `${id}-helper` : undefined;
+
+ const wrapperRef = useRef(null);
+ const [isRtl, setIsRtl] = useState(false);
+
+ useEffect(() => {
+ const wrapper = wrapperRef.current;
+ if (!wrapper) return undefined;
+
+ const readDirection = () => {
+ setIsRtl(window.getComputedStyle(wrapper).direction === 'rtl');
+ };
+
+ readDirection();
+ const observer = new MutationObserver(readDirection);
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
+
+ return () => observer.disconnect();
+ }, []);
+
+ const outerTheme = useTheme();
+ const theme = useMemo(
+ () => (isRtl && outerTheme.direction !== 'rtl'
+ ? createTheme(outerTheme, { direction: 'rtl' })
+ : outerTheme),
+ [outerTheme, isRtl],
+ );
+
+ const handleChange = useCallback((event: Event, value: number | number[]) => {
+ onChange?.(event, Array.isArray(value) ? value[0] : value);
+ }, [onChange]);
+
+ const handleChangeCommitted = useCallback((
+ event: React.SyntheticEvent | Event,
+ value: number | number[],
+ ) => {
+ onChangeCommitted?.(event, Array.isArray(value) ? value[0] : value);
+ }, [onChangeCommitted]);
+
+ return (
+
+ {(label || helperText) && (
+
+ {label && {label}}
+ {helperText && {helperText}}
+
+ )}
+
+ {iconStart && {iconStart}}
+
+
+
+ {iconEnd && {iconEnd}}
+
+
+ );
+}
+
+export default Slider;
diff --git a/src/components/Slider/constants.ts b/src/components/Slider/constants.ts
new file mode 100644
index 0000000..628d7fa
--- /dev/null
+++ b/src/components/Slider/constants.ts
@@ -0,0 +1,10 @@
+export const VALUE_LABEL_DISPLAYS = {
+ AUTO: 'auto',
+ ON: 'on',
+ OFF: 'off',
+} as const;
+
+export const VALUE_LABEL_DISPLAY_VALUES = Object.values(VALUE_LABEL_DISPLAYS);
+export const DEFAULT_VALUE_LABEL_DISPLAY = VALUE_LABEL_DISPLAYS.AUTO;
+export const DEFAULT_SHOW_MARK_TICKS = true;
+export const DEFAULT_ANIMATE = true;
diff --git a/src/components/Slider/index.ts b/src/components/Slider/index.ts
new file mode 100644
index 0000000..58fd15e
--- /dev/null
+++ b/src/components/Slider/index.ts
@@ -0,0 +1 @@
+export { default as BBBSlider } from './component';
diff --git a/src/components/Slider/styles.ts b/src/components/Slider/styles.ts
new file mode 100644
index 0000000..edd3929
--- /dev/null
+++ b/src/components/Slider/styles.ts
@@ -0,0 +1,167 @@
+import { Slider } from '@mui/material';
+import { styled as materialStyled } from '@mui/material/styles';
+import styled from 'styled-components';
+import {
+ colorBrand1,
+ colorNeutral3,
+ colorWhite,
+ colorShadowDefault,
+ colorTextDefault,
+ colorTextLight,
+ colorIconDefault,
+ colorBorderFocusRing,
+} from '../../stylesheets/palette';
+import {
+ fontSizeXSmall,
+ fontSizeDefault,
+ fontSizeBig,
+ fontWeightDefault,
+ fontWeightLight,
+} from '../../stylesheets/typography';
+import {
+ spacingXSmall,
+ spacingSmall,
+ spacingXLarge,
+ borderRadiusSmall,
+} from '../../stylesheets/sizing';
+import { StyledSliderRowProps, StyledSliderWrapperProps } from './types';
+
+const THUMB_SIZE = '1rem';
+const TRACK_HEIGHT = '0.3125rem';
+const MARK_SIZE = '0.25rem';
+const ICON_SIZE = '1.5rem';
+
+export const SliderWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ ${({ $valueLabelSpace }) => $valueLabelSpace && `
+ gap: ${spacingXLarge};
+ `}
+`;
+
+export const TextWrapper = styled.label`
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+`;
+
+export const Title = styled.span`
+ color: ${colorTextDefault};
+ font-size: ${fontSizeBig};
+ font-weight: ${fontWeightLight};
+`;
+
+export const HelperText = styled.span`
+ color: ${colorTextLight};
+ font-size: ${fontSizeDefault};
+ font-weight: ${fontWeightLight};
+`;
+
+export const SliderRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${spacingSmall};
+ width: 100%;
+
+ ${({ $showMarkTicks }) => !$showMarkTicks && `
+ & .MuiSlider-mark {
+ display: none;
+ }
+ `}
+
+ ${({ $animate }) => !$animate && `
+ & .MuiSlider-thumb,
+ & .MuiSlider-track,
+ & .MuiSlider-valueLabel {
+ transition: none;
+ }
+
+ & .MuiSlider-thumb:hover:not(.Mui-focusVisible),
+ & .MuiSlider-thumb.Mui-active:not(.Mui-focusVisible) {
+ box-shadow: 0 2px 4px 0 ${colorShadowDefault};
+ }
+ `}
+
+ ${({ $isRtl }) => $isRtl && `
+ /* MUI only mirrors right:-anchored positioning under RTL, not the LTR transforms paired
+ with it (that half normally comes from stylis-plugin-rtl, which this library can't add) */
+ && .MuiSlider-thumb {
+ transform: translate(50%, -50%);
+ }
+
+ && .MuiSlider-mark {
+ transform: translate(1px, -50%);
+ }
+
+ && .MuiSlider-markLabel {
+ transform: translateX(50%);
+ }
+ `}
+`;
+
+export const IconWrapper = styled.span`
+ display: flex;
+ align-items: center;
+ color: ${colorIconDefault};
+ font-size: ${ICON_SIZE};
+`;
+
+export const MaterialSlider = materialStyled(Slider)({
+ color: colorBrand1,
+ height: TRACK_HEIGHT,
+ '& .MuiSlider-rail': {
+ backgroundColor: 'currentColor',
+ opacity: 0.38,
+ },
+ '& .MuiSlider-track': {
+ border: '1px solid currentColor',
+ backgroundColor: colorBrand1,
+ },
+ '& .MuiSlider-thumb': {
+ height: THUMB_SIZE,
+ width: THUMB_SIZE,
+ backgroundColor: 'currentColor',
+ boxShadow: `0 2px 4px 0 ${colorShadowDefault}`,
+ '&:hover, &.Mui-focusVisible': {
+ boxShadow: `0 0 0 0.5rem ${colorBorderFocusRing}`,
+ },
+ '&.Mui-active': {
+ boxShadow: `0 0 0 0.875rem ${colorBorderFocusRing}`,
+ },
+ },
+ '& .MuiSlider-mark': {
+ width: MARK_SIZE,
+ height: MARK_SIZE,
+ borderRadius: '50%',
+ backgroundColor: colorNeutral3,
+ '&.MuiSlider-markActive': {
+ backgroundColor: colorWhite,
+ },
+ },
+ '& .MuiSlider-markLabel': {
+ color: colorTextLight,
+ fontSize: fontSizeXSmall,
+ // RTL: resolve each label's own direction so a leading minus sign doesn't get reordered
+ unicodeBidi: 'plaintext',
+ },
+ '& .MuiSlider-valueLabel': {
+ backgroundColor: colorTextDefault,
+ color: colorWhite,
+ fontSize: fontSizeXSmall,
+ fontWeight: fontWeightDefault,
+ borderRadius: borderRadiusSmall,
+ padding: `${spacingXSmall} ${spacingSmall}`,
+ '& .MuiSlider-valueLabelLabel': {
+ // RTL: resolve each label's own direction so a leading minus sign doesn't get reordered
+ unicodeBidi: 'plaintext',
+ },
+ },
+ '&.Mui-disabled': {
+ color: colorNeutral3,
+ '& .MuiSlider-track': {
+ backgroundColor: colorNeutral3,
+ },
+ },
+});
diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts
new file mode 100644
index 0000000..6e1a6a7
--- /dev/null
+++ b/src/components/Slider/types.ts
@@ -0,0 +1,67 @@
+import React from 'react';
+import { SliderProps as MuiSliderProps } from '@mui/material';
+import { VALUE_LABEL_DISPLAY_VALUES } from './constants';
+
+export type ValueLabelDisplay = typeof VALUE_LABEL_DISPLAY_VALUES[number];
+
+export interface StyledSliderRowProps {
+ $showMarkTicks: boolean;
+ $animate: boolean;
+ $isRtl: boolean;
+}
+
+export interface StyledSliderWrapperProps {
+ $valueLabelSpace: boolean;
+}
+
+export interface SliderProps extends Omit {
+ /** Main label text displayed above the slider. */
+ label?: string;
+
+ /** Helper text displayed below the label. */
+ helperText?: string;
+
+ /** Current value; providing it makes the slider controlled. */
+ value?: number;
+
+ /** Initial value used while the slider is uncontrolled. */
+ defaultValue?: number;
+
+ /** Change handler fired continuously while the user drags or presses a key. */
+ onChange?: (event: Event, value: number) => void;
+
+ /** Change handler fired once, when the interaction ends; prefer it for expensive updates. */
+ onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number) => void;
+
+ /** Decorative node rendered at the minimum end of the track. */
+ iconStart?: React.ReactNode;
+
+ /** Decorative node rendered at the maximum end of the track. */
+ iconEnd?: React.ReactNode;
+
+ /** Controls when the value tooltip is shown. @default 'auto' */
+ valueLabelDisplay?: ValueLabelDisplay;
+
+ /** Renders the tick dots of `marks`; set to false to keep the mark labels only. @default true */
+ showMarkTicks?: boolean;
+
+ /** Enables transitions and hover/drag visual effects; set to false for instant state changes. @default true */
+ animate?: boolean;
+
+ /** HTML `id` applied to the underlying range input; auto-generated via `useId` when omitted. */
+ id?: string;
+
+ /** Value for the `data-test` attribute on the root; the range input gets `${dataTest}-input`. */
+ dataTest?: string;
+
+ /** Accessible name for the slider. */
+ ariaLabel?: string;
+
+ /** ID of the element that labels the slider; ignored when `ariaLabel` is set, and falls back to the rendered label's id when omitted. */
+ ariaLabelledBy?: string;
+
+ /** ID of the element that describes the slider; falls back to the rendered helper text's id when omitted. */
+ ariaDescribedBy?: string;
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index 230d706..6c16524 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -9,6 +9,7 @@ export { BBBNavigation } from './Navigation';
export { BBBScrollArea } from './ScrollArea';
export { BBBSearch } from './Search';
export { BBBSelect } from './Select';
+export { BBBSlider } from './Slider';
export { BBBSpinner } from './Spinner';
export { BBBTextAreaInput } from './TextAreaInput';
export { BBBTextInput } from './TextInput';
diff --git a/webpack.config.babel.js b/webpack.config.babel.js
index 461ae7f..49b575f 100644
--- a/webpack.config.babel.js
+++ b/webpack.config.babel.js
@@ -13,6 +13,7 @@ export default {
ScrollArea: './src/components/ScrollArea/index.ts',
Search: './src/components/Search/index.ts',
Select: './src/components/Select/index.ts',
+ Slider: './src/components/Slider/index.ts',
Spinner: './src/components/Spinner/index.ts',
TextAreaInput: './src/components/TextAreaInput/index.ts',
TextInput: './src/components/TextInput/index.ts',