+ ),
+ ],
+};
+
+export const FieldNote: StoryObj = {
+ args: {
+ label: 'Slider Label',
+ fieldNote: 'This is a fieldnote. It overrides the markers',
+ markers: 'number',
+ },
+};
+
+// For visual regression test
+export const Focus: StoryObj = {
+ args: {
+ label: 'Slider Label',
+ },
+ parameters: {
+ /**
+ * No point snapping the button as this story is testing visual regression on the focus state (snap no difference than Default story).
+ */
+ snapshot: { skip: true },
+ },
+ play: () => {
+ userEvent.tab();
+ },
+};
diff --git a/src/components/Slider/Slider.test.tsx b/src/components/Slider/Slider.test.tsx
new file mode 100644
index 000000000..48ea1963c
--- /dev/null
+++ b/src/components/Slider/Slider.test.tsx
@@ -0,0 +1,36 @@
+import { generateSnapshots } from '@chanzuckerberg/story-utils';
+import { render } from '@testing-library/react';
+import React from 'react';
+import * as stories from './Slider.stories';
+import Slider from './';
+
+describe('', () => {
+ generateSnapshots(stories);
+ describe('error throws', () => {
+ // expect console error from react, suppressed.
+ const consoleErrorMock = jest.spyOn(console, 'error');
+ consoleErrorMock.mockImplementation();
+ it('throws an error if no label or aria-label', () => {
+ expect(() => {
+ render();
+ }).toThrow(/You must provide a visible label or aria-label/);
+ });
+ it('throws an error if told to generate markers, but steps are not integers', () => {
+ expect(() => {
+ render(
+ ,
+ );
+ }).toThrow(
+ /Number of markers is not an integer. Change step or supply custom markers/,
+ );
+ });
+ consoleErrorMock.mockRestore();
+ });
+});
diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx
new file mode 100644
index 000000000..32cb9e23c
--- /dev/null
+++ b/src/components/Slider/Slider.tsx
@@ -0,0 +1,182 @@
+import clsx from 'clsx';
+import React, {
+ useId,
+ type ChangeEventHandler,
+ type CSSProperties,
+} from 'react';
+import { findLowestTenMultiplier } from '../../util/findLowestTenMultiplier';
+import FieldNote from '../FieldNote';
+import Label from '../Label';
+import Text from '../Text';
+import styles from './Slider.module.css';
+
+export type Props = React.InputHTMLAttributes & {
+ /**
+ * Aria-label to provide an accesible name for the text input if no visible label is provided.
+ */
+ 'aria-label'?: string;
+ /**
+ * CSS class names that can be appended to the component.
+ */
+ className?: string;
+ /**
+ * Disables the slider and prevents change.
+ */
+ disabled?: boolean;
+ /**
+ * Text under the text input used to describe the slider.
+ * Will override the markers if present.
+ */
+ fieldNote?: React.ReactNode;
+ /**
+ * HTML id for the component
+ */
+ id?: string;
+ /**
+ * HTML label text
+ */
+ label?: string;
+ /**
+ * List of markers to imply slider value.
+ * As 'number', will automatically generate markers based on min, max, and step.
+ * As an array of strings, will space the strings apart evenly.
+ */
+ markers?: 'number' | string[];
+ /**
+ * Maximum value allowed for the slider.
+ */
+ max: number;
+ /**
+ * Minimum value allowed for the slider.
+ */
+ min: number;
+ /**
+ * Function that runs on change of the input
+ */
+ onChange?: ChangeEventHandler;
+ /**
+ * Amount to increment each step by.
+ */
+ step: number;
+ /**
+ * Value denoted by the slider.
+ */
+ value: number;
+};
+
+/**
+ * BETA: This component is still a work in progress and is subject to change.
+ *
+ * `import {Slider} from "@chanzuckerberg/eds";`
+ *
+ * Allows input of a value via dragging a thumb along a track.
+ * Strict: This slider requires a visual indicator of value/markers.
+ * Please check out our recipes for possible ideas.
+ */
+export const Slider = ({
+ className,
+ disabled,
+ fieldNote,
+ id,
+ label,
+ markers,
+ max,
+ min,
+ step,
+ value,
+ ...other
+}: Props) => {
+ if (process.env.NODE_ENV !== 'production' && !label && !other['aria-label']) {
+ throw new Error('You must provide a visible label or aria-label');
+ }
+
+ // Required due to 0.1 + 0.2 != 0.3
+ const multiplier = findLowestTenMultiplier([max, min, step]);
+ const markersCount =
+ (max * multiplier - min * multiplier) / (step * multiplier) + 1;
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ markers === 'number' &&
+ !Number.isInteger(markersCount)
+ ) {
+ throw new Error(
+ 'Number of markers is not an integer. Change step or supply custom markers',
+ );
+ }
+
+ const componentClassName = clsx(styles['slider'], className);
+ const labelClassName = clsx(disabled && styles['slider__label--disabled']);
+ const markerClassName = clsx(
+ styles['slider__marker'],
+ disabled && styles['slider__marker--disabled'],
+ );
+
+ const generatedId = useId();
+ const sliderId = id || generatedId;
+
+ const generatedAriaDescribedById = useId();
+ const ariaDescribedByVar = fieldNote
+ ? other['aria-describedby'] || generatedAriaDescribedById
+ : undefined;
+
+ return (
+