diff --git a/packages/main/src/SliderEvolution.ts b/packages/main/src/SliderEvolution.ts
new file mode 100644
index 000000000000..a50aa4774bfb
--- /dev/null
+++ b/packages/main/src/SliderEvolution.ts
@@ -0,0 +1,123 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
+import SliderEvolutionTemplate from "./SliderEvolutionTemplate.js";
+import SliderScale, { SliderScaleOrientation } from "./SliderScale.js";
+import SliderHandle from "./SliderHandle.js";
+import styles from "./generated/themes/SliderEvolution.css.js";
+
+@customElement({
+ tag: "ui5-slider-evolution",
+ renderer: jsxRenderer,
+ template: SliderEvolutionTemplate,
+ styles,
+ dependencies: [SliderScale, SliderHandle],
+})
+class SliderEvolution extends UI5Element {
+ @property({ type: Boolean, noAttribute: true })
+ _pressed = false;
+
+ @property({ type: Number })
+ value = 0;
+
+ @property({ type: Number })
+ min = 0;
+
+ @property({ type: Number })
+ max = 100;
+
+ @property({ type: Number })
+ step = 1;
+
+ @property({ type: Boolean })
+ showTickmarks = false;
+
+ @property({ type: Boolean })
+ showTickmarkLabels = false;
+
+ @property()
+ orientation: `${SliderScaleOrientation}` = "Horizontal";
+
+ get _handlePosition() {
+ const range = this.max - this.min;
+ const position = ((this.value - this.min) / range) * 100;
+ return position;
+ }
+
+ _onmousedown = (e: MouseEvent) => {
+ const target = e.target as HTMLElement;
+
+ this._pressed = true;
+
+ if (!this.getDomRef()?.contains(target) || !target.hasAttribute("ui5-slider-handle")) {
+ this._updateValue(e);
+ }
+
+ document.addEventListener("mouseup", this._onmouseup);
+ document.addEventListener("mousemove", this._onmousemove);
+ };
+
+ _onmouseup = () => {
+ this._pressed = false;
+ document.removeEventListener("mouseup", this._onmouseup);
+ document.removeEventListener("mousemove", this._onmousemove);
+ };
+
+ _onmousemove = (e: MouseEvent) => {
+ if (this._pressed) {
+ this._updateValue(e);
+ }
+ };
+
+ _updateValue(e: MouseEvent) {
+ const rect = this.getBoundingClientRect();
+ let percentage = 0;
+
+ if (this.orientation === SliderScaleOrientation.Horizontal) {
+ if (this.effectiveDir === "rtl") {
+ const x = e.clientX - rect.left;
+ percentage = 1 - x / rect.width;
+ } else {
+ const x = e.clientX - rect.left;
+ percentage = x / rect.width;
+ }
+ } else {
+ const y = e.clientY - rect.top;
+ percentage = 1 - y / rect.height;
+ }
+
+ let value = this.min + percentage * (this.max - this.min);
+
+ value = Math.round(value / this.step) * this.step;
+
+ if (value < this.min) {
+ value = this.min;
+ }
+ if (value > this.max) {
+ value = this.max;
+ }
+
+ this.value = value;
+ }
+
+ keydown(e: KeyboardEvent) {
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
+ this.value = Math.min(this.value + this.step, this.max);
+ e.preventDefault();
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
+ this.value = Math.max(this.value - this.step, this.min);
+ e.preventDefault();
+ } else if (e.key === "Home") {
+ this.value = this.min;
+ e.preventDefault();
+ } else if (e.key === "End") {
+ this.value = this.max;
+ e.preventDefault();
+ }
+ };
+}
+
+SliderEvolution.define();
+
+export default SliderEvolution;
diff --git a/packages/main/src/SliderEvolutionTemplate.tsx b/packages/main/src/SliderEvolutionTemplate.tsx
new file mode 100644
index 000000000000..481eeeeb62c3
--- /dev/null
+++ b/packages/main/src/SliderEvolutionTemplate.tsx
@@ -0,0 +1,58 @@
+import type SliderEvolution from "./SliderEvolution.js";
+import SliderScale from "./SliderScale.js";
+import SliderHandle from "./SliderHandle.js";
+import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScopeUtils.js";
+
+export default function SliderEvolutionTemplate(this: SliderEvolution) {
+ const _handlePosition = () => {
+ const range = this.max - this.min;
+ const position = ((this.value - this.min) / range) * 100;
+ return position;
+ };
+
+ const _handleVerticalPosition = () => {
+ const range = this.max - this.min;
+ const position = ((this.value - this.min) / range) * 100;
+ return position;
+ };
+
+ const calcHandlePosition = () => {
+ if (this.orientation === "Vertical") {
+ return `calc(${_handleVerticalPosition()}% - calc(var(${getScopedVarName("--_ui5_slider_handle_height")}) / 2))`;
+ }
+
+ if (this.effectiveDir === "rtl") {
+ return `calc(${100 - _handlePosition()}% - calc(var(${getScopedVarName("--_ui5_slider_handle_width")}) / 2))`;
+ }
+
+ return `calc(${_handlePosition()}% - calc(var(${getScopedVarName("--_ui5_slider_handle_width")}) / 2))`;
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/main/src/SliderHandle.ts b/packages/main/src/SliderHandle.ts
new file mode 100644
index 000000000000..08d7bd69808c
--- /dev/null
+++ b/packages/main/src/SliderHandle.ts
@@ -0,0 +1,43 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
+import SliderHandleTemplate from "./SliderHandleTemplate.js";
+import styles from "./generated/themes/SliderHandle.css.js";
+import type { SliderScaleOrientation } from "./SliderScale.js";
+
+@customElement({
+ tag: "ui5-slider-handle",
+ renderer: jsxRenderer,
+ template: SliderHandleTemplate,
+ styles,
+})
+class SliderHandle extends UI5Element {
+ @property({ type: Number })
+ value = 0;
+
+ @property({ type: Number })
+ min = 0;
+
+ @property({ type: Number })
+ max = 100;
+
+ @property({ type: Boolean })
+ disabled = false;
+
+ @property({ type: Boolean })
+ active = false;
+
+ @property()
+ orientation: `${SliderScaleOrientation}` = "Horizontal";
+
+ get _handlePosition() {
+ const range = this.max - this.min;
+ const position = ((this.value - this.min) / range) * 100;
+ return position;
+ }
+}
+
+SliderHandle.define();
+
+export default SliderHandle;
diff --git a/packages/main/src/SliderHandleTemplate.tsx b/packages/main/src/SliderHandleTemplate.tsx
new file mode 100644
index 000000000000..eaf9beac5a1f
--- /dev/null
+++ b/packages/main/src/SliderHandleTemplate.tsx
@@ -0,0 +1,15 @@
+import directionArrows from "@ui5/webcomponents-icons/dist/direction-arrows.js";
+import Icon from "./Icon.js";
+import type SliderHandle from "./SliderHandle.js";
+
+export default function SliderHandleTemplate(this: SliderHandle) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/main/src/SliderScale.ts b/packages/main/src/SliderScale.ts
new file mode 100644
index 000000000000..61f4ba777880
--- /dev/null
+++ b/packages/main/src/SliderScale.ts
@@ -0,0 +1,226 @@
+import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
+import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
+import property from "@ui5/webcomponents-base/dist/decorators/property.js";
+import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
+import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
+import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
+import SliderScaleTemplate from "./SliderScaleTemplate.js";
+
+import SliderScaleCss from "./generated/themes/SliderScale.css.js";
+
+type Tickmark = {
+ value: number;
+ label?: string;
+};
+
+enum SliderScaleOrientation {
+ /**
+ * Horizontal orientation
+ * @public
+ */
+ Horizontal = "Horizontal",
+
+ /**
+ * Vertical orientation
+ * @public
+ */
+ Vertical = "Vertical",
+}
+
+@customElement({
+ tag: "ui5-slider-scale",
+ renderer: jsxRenderer,
+ styles: SliderScaleCss,
+ template: SliderScaleTemplate,
+})
+class SliderScale extends UI5Element {
+ @property({ type: Number })
+ startValue = 0;
+
+ @property({ type: Number })
+ endValue = 100;
+
+ @property({ type: Number })
+ min = 0;
+
+ @property({ type: Number })
+ max = 100;
+
+ @property({ type: Number })
+ step = 1;
+
+ @property({ type: Boolean })
+ showTickmarks = false;
+
+ @property({ type: Boolean })
+ showTickmarkLabels = false;
+
+ @property()
+ orientation: `${SliderScaleOrientation}` = "Horizontal";
+
+ @property({ type: Array })
+ tickmarks: Array = [];
+
+ /**
+ * @private
+ */
+ @property({ type: Number })
+ _labelInterval = 1;
+
+ _resizeHandler: ResizeObserverCallback;
+ _notResized = true;
+
+ static get MIN_LABEL_DISTANCE() {
+ return 16;
+ }
+
+ constructor() {
+ super();
+ this._resizeHandler = this._handleResize.bind(this);
+ }
+
+ onEnterDOM() {
+ ResizeHandler.register(this, this._resizeHandler);
+ }
+
+ onExitDOM() {
+ ResizeHandler.deregister(this, this._resizeHandler);
+ }
+
+ onAfterRendering() {
+ if (this._notResized) {
+ this._handleResize();
+ }
+ }
+
+ /**
+ * Handles resize to determine optimal label interval
+ * @private
+ */
+ _handleResize() {
+ if (!this.showTickmarkLabels) {
+ return;
+ }
+
+ this._notResized = false;
+
+ const width = this.orientation === SliderScaleOrientation.Horizontal
+ ? this.getBoundingClientRect().width
+ : this.getBoundingClientRect().height;
+ const totalTickmarks = this._allTickmarks.length;
+
+ if (totalTickmarks <= 1) {
+ this._labelInterval = 1;
+ return;
+ }
+
+ // Start with showing all labels (interval = 1)
+ let interval = 1;
+ let visibleLabelCount = Math.floor((totalTickmarks - 1) / interval) + 1;
+ let spaceBetweenLabels = width / (visibleLabelCount - 1);
+
+ // Keep doubling the interval until we have enough space
+ while (
+ spaceBetweenLabels < SliderScale.MIN_LABEL_DISTANCE && interval < totalTickmarks
+ ) {
+ interval *= 2;
+ visibleLabelCount = Math.floor((totalTickmarks - 1) / interval) + 1;
+ spaceBetweenLabels = width / (visibleLabelCount - 1);
+ }
+
+ this._labelInterval = interval;
+ }
+
+ get _progressStyle() {
+ const range = this.max - this.min;
+ const start = ((this.startValue - this.min) / range) * 100;
+ const end = ((this.endValue - this.min) / range) * 100;
+
+ if (this.orientation === SliderScaleOrientation.Vertical) {
+ return {
+ top: "auto",
+ bottom: `${start}%`,
+ height: `${end - start}%`,
+ };
+ }
+
+ return {
+ insetInlineStart: `${start}%`,
+ width: `${end - start}%`,
+ };
+ }
+
+ get _allTickmarks() {
+ // If custom tickmarks are provided, use them
+ if (this.tickmarks.length > 0) {
+ return this.tickmarks;
+ }
+
+ // Otherwise, generate tickmarks based on step
+ if (!this.showTickmarks) {
+ return [];
+ }
+
+ const values = [];
+ for (let value = this.min; value <= this.max; value += this.step) {
+ values.push({ value });
+ }
+ return values;
+ }
+
+ get _tickmarks() {
+ const allTickmarks = this._allTickmarks;
+
+ if (allTickmarks.length === 0) {
+ return [];
+ }
+
+ // If labels are not shown, show all tickmarks without labels
+ if (!this.showTickmarkLabels) {
+ return allTickmarks.map(tm => {
+ const value = tm.value;
+ const isInRange = value >= this.startValue && value <= this.endValue;
+ const position = ((value - this.min) / (this.max - this.min)) * 100;
+
+ return {
+ value,
+ label: undefined,
+ isInRange,
+ position,
+ showLabel: false,
+ };
+ });
+ }
+
+ // If labels are shown, only show tickmarks that have labels
+ const tickmarksWithLabels:Array<{value: number; label: string; isInRange: boolean; position: number; showLabel: boolean}> = [];
+
+ allTickmarks.forEach((tm, index) => {
+ const value = tm.value;
+ const isFirstOrLast = index === 0 || index === allTickmarks.length - 1;
+ const isIntervalMatch = index % this._labelInterval === 0;
+
+ // Only include this tickmark if its label should be shown
+ if (isFirstOrLast || isIntervalMatch) {
+ const isInRange = value >= this.startValue && value <= this.endValue;
+ const position = ((value - this.min) / (this.max - this.min)) * 100;
+
+ tickmarksWithLabels.push({
+ value,
+ label: tm.label || value.toString(),
+ isInRange,
+ position,
+ showLabel: true,
+ });
+ }
+ });
+
+ return tickmarksWithLabels;
+ }
+}
+
+SliderScale.define();
+
+export default SliderScale;
+export { SliderScaleOrientation };
+export type { Tickmark };
diff --git a/packages/main/src/SliderScaleTemplate.tsx b/packages/main/src/SliderScaleTemplate.tsx
new file mode 100644
index 000000000000..b6cd1d15ad71
--- /dev/null
+++ b/packages/main/src/SliderScaleTemplate.tsx
@@ -0,0 +1,31 @@
+import type SliderScale from "./SliderScale.js";
+
+export default function SliderScaleTemplate(this: SliderScale) {
+ return (
+
+ {this._tickmarks.length > 0 && (
+
+ {this._tickmarks.map(tick => (
+
+ {tick.label && tick.showLabel && (
+
+ {tick.label}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts
index 77c873528352..c1fbe786b6b5 100644
--- a/packages/main/src/bundle.esm.ts
+++ b/packages/main/src/bundle.esm.ts
@@ -97,6 +97,9 @@ import Select from "./Select.js";
import Option from "./Option.js";
import CustomOption from "./OptionCustom.js";
import Slider from "./Slider.js";
+import SliderScale from "./SliderScale.js";
+import SliderHandle from "./SliderHandle.js";
+import SliderEvolution from "./SliderEvolution.js";
import SplitButton from "./SplitButton.js";
import StepInput from "./StepInput.js";
import RangeSlider from "./RangeSlider.js";
diff --git a/packages/main/src/themes/SliderEvolution.css b/packages/main/src/themes/SliderEvolution.css
new file mode 100644
index 000000000000..fe2a299f7c58
--- /dev/null
+++ b/packages/main/src/themes/SliderEvolution.css
@@ -0,0 +1,28 @@
+:host {
+ display: inline-block;
+ width: 100%;
+ height: 3rem;
+ box-sizing: border-box;
+ user-select: none;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0;
+}
+
+.ui5-slider-evolution-root {
+ position: relative;
+ width: 100%;
+ height: 3rem;
+ cursor: pointer;
+}
+
+:host([orientation="Vertical"]) {
+ width: 3rem;
+ height: 100%;
+ padding: 0 0.5rem;
+
+ .ui5-slider-evolution-root {
+ width: 3rem;
+ height: 100%;
+ }
+}
diff --git a/packages/main/src/themes/SliderHandle.css b/packages/main/src/themes/SliderHandle.css
new file mode 100644
index 000000000000..06218e3d3c0d
--- /dev/null
+++ b/packages/main/src/themes/SliderHandle.css
@@ -0,0 +1,48 @@
+:host {
+ background: var(--_ui5_slider_handle_background);
+ border: var(--_ui5_slider_handle_border);
+ border-radius: var(--_ui5_slider_handle_border_radius);
+ position: absolute;
+ outline: none;
+ height: var(--_ui5_slider_handle_height);
+ width: var(--_ui5_slider_handle_width);
+ box-sizing: var(--_ui5_slider_handle_box_sizing);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 2;
+ top: 0;
+ cursor: pointer;
+}
+
+.ui5-slider-handle-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ outline: none;
+}
+
+[slider-icon] {
+ display: var(--_ui5_slider_handle_icon_display);
+ color: var(--sapContent_Selected_ForegroundColor);
+ width: var(--_ui5_slider_handle_icon_size);
+ height: var(--_ui5_slider_handle_icon_size);
+}
+
+:host(:focus),
+:host([active]),
+:host(:active) {
+ [slider-icon] {
+ display: none;
+ }
+
+ background-color: transparent;
+ border: var(--_ui5_slider_handle_focus_border);
+}
+
+:host([orientation="Vertical"]) {
+ /* rotate */
+ transform: rotate(90deg);
+}
\ No newline at end of file
diff --git a/packages/main/src/themes/SliderScale.css b/packages/main/src/themes/SliderScale.css
new file mode 100644
index 000000000000..d078f4503f79
--- /dev/null
+++ b/packages/main/src/themes/SliderScale.css
@@ -0,0 +1,136 @@
+:host {
+ height: 2rem;
+ position: relative;
+ width: 100%;
+ box-sizing: border-box;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.ui5-slider-scale-root {
+ height: .25rem;
+}
+
+:host([orientation="Vertical"]) {
+ width: 2rem;
+ height: 100%;
+
+ .ui5-slider-scale-root {
+ box-sizing: border-box;
+ height: 100%;
+ width: 0.25rem;
+ background-color: var(--sapSlider_Background);
+ position: relative;
+ }
+
+ .ui5-slider-scale-progress {
+ width: 100%;
+ height: auto;
+ }
+
+ .ui5-slider-scale-root:before {
+ left: 50%;
+ top: -12px;
+ right: auto;
+ transform: translateX(-50%);
+ }
+
+ .ui5-slider-scale-root:after {
+ left: 50%;
+ right: auto;
+ top: auto;
+ bottom: -12px;
+ transform: translateX(-50%);
+ }
+
+ .ui5-slider-scale-tickmark-label {
+ margin-top: 0;
+ margin-left: 14px;
+ top: 50%;
+ left: 100%;
+ transform: translateY(-50%);
+ }
+
+ .ui5-slider-scale-tickmark {
+ height: auto;
+ width: .5rem;
+ margin-top: 0;
+ margin-left: 0;
+ border-left: none;
+ border-top: 0.0625rem solid var(--sapField_BorderColor);
+ }
+}
+
+.ui5-slider-scale-root {
+ box-sizing: border-box;
+ border-radius: 0.25rem;
+ width: 100%;
+ background-color: var(--sapSlider_Background);
+ position: relative;
+}
+
+.ui5-slider-scale-tickmarks-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ z-index: 1;
+}
+
+.ui5-slider-scale-tickmark {
+ position: absolute;
+ height: .5rem;
+ margin-top: -.125rem;
+ transform: translateX(-50%);
+ border-left: 0.0625rem solid var(--sapField_BorderColor);
+}
+
+.ui5-slider-scale-tickmark-in-range {
+ border-left-color: var(--sapSlider_Selected_BorderColor);
+}
+
+.ui5-slider-scale-tickmark-label {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 0.875rem;
+ font-size: var(--sapFontSmallSize);
+ color: var(--sapContent_LabelColor);
+ white-space: nowrap;
+}
+
+.ui5-slider-scale-progress {
+ position: absolute;
+ height: 100%;
+ top: 0;
+ background-color: var(--sapSlider_Selected_Background);
+ z-index: 2;
+}
+
+.ui5-slider-scale-root::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 0;
+ width: .5rem;
+ height: .5rem;
+ background-color: var(--sapContent_MeasureIndicatorColor);
+ border-radius: 50%;
+ left: -12px;
+ top: -50%;
+}
+
+.ui5-slider-scale-root::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ right: 0;
+ width: .5rem;
+ height: .5rem;
+ background-color: var(--sapContent_MeasureIndicatorColor);
+ border-radius: 50%;
+ right: -12px;
+ top: -50%;
+}
\ No newline at end of file
diff --git a/packages/main/test/pages/SliderScale.html b/packages/main/test/pages/SliderScale.html
new file mode 100644
index 000000000000..e4925756be93
--- /dev/null
+++ b/packages/main/test/pages/SliderScale.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+ UI5 Slider
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Slider Evolution examples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+