diff --git a/android-design-system/design-system-internal/src/main/java/com/duckduckgo/common/ui/internal/ui/component/ComponentViewHolder.kt b/android-design-system/design-system-internal/src/main/java/com/duckduckgo/common/ui/internal/ui/component/ComponentViewHolder.kt
index 64ff4d6428c8..c2714081931f 100644
--- a/android-design-system/design-system-internal/src/main/java/com/duckduckgo/common/ui/internal/ui/component/ComponentViewHolder.kt
+++ b/android-design-system/design-system-internal/src/main/java/com/duckduckgo/common/ui/internal/ui/component/ComponentViewHolder.kt
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -91,6 +92,7 @@ import com.duckduckgo.common.ui.compose.progress.DaxProgressSpinner
import com.duckduckgo.common.ui.compose.radiobutton.DaxRadioButton
import com.duckduckgo.common.ui.compose.skeleton.DaxSkeletonListItem
import com.duckduckgo.common.ui.compose.skeleton.DaxSkeletonSectionHeader
+import com.duckduckgo.common.ui.compose.slider.DaxSlider
import com.duckduckgo.common.ui.compose.snackbar.DaxSnackbar
import com.duckduckgo.common.ui.compose.switch.DaxSwitch
import com.duckduckgo.common.ui.compose.text.DaxText
@@ -295,8 +297,36 @@ sealed class ComponentViewHolder(val view: View) : RecyclerView.ViewHolder(view)
}
}
- class SliderComponentViewHolder(parent: ViewGroup) :
- ComponentViewHolder(inflate(parent, R.layout.component_slider))
+ class SliderComponentViewHolder(
+ parent: ViewGroup,
+ private val isDarkTheme: Boolean,
+ ) : ComponentViewHolder(inflate(parent, R.layout.component_slider)) {
+
+ init {
+ view.setupThemedComposeView(R.id.composeDaxSlider, isDarkTheme) {
+ var continuousValue by remember { mutableFloatStateOf(0.3f) }
+ var steppedValue by remember { mutableFloatStateOf(100f) }
+ var disabledValue by remember { mutableFloatStateOf(0.3f) }
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ DaxSlider(
+ value = continuousValue,
+ onValueChange = { continuousValue = it },
+ )
+ DaxSlider(
+ value = steppedValue,
+ onValueChange = { steppedValue = it },
+ valueRange = 70f..170f,
+ steps = 9,
+ )
+ DaxSlider(
+ value = disabledValue,
+ onValueChange = { disabledValue = it },
+ enabled = false,
+ )
+ }
+ }
+ }
+ }
class InfoPanelComponentViewHolder(
parent: ViewGroup,
@@ -1037,7 +1067,7 @@ sealed class ComponentViewHolder(val view: View) : RecyclerView.ViewHolder(view)
Component.SWITCH -> SwitchComponentViewHolder(parent, isDarkTheme)
Component.RADIO_BUTTON -> RadioButtonComponentViewHolder(parent, isDarkTheme)
Component.CHECKBOX -> CheckboxComponentViewHolder(parent, isDarkTheme)
- Component.SLIDER -> SliderComponentViewHolder(parent)
+ Component.SLIDER -> SliderComponentViewHolder(parent, isDarkTheme)
Component.SNACKBAR -> SnackbarComponentViewHolder(parent, isDarkTheme)
Component.INFO_PANEL -> InfoPanelComponentViewHolder(parent, isDarkTheme)
Component.REMOTE_MESSAGE -> RemoteMessageComponentViewHolder(parent, isDarkTheme)
diff --git a/android-design-system/design-system-internal/src/main/res/layout/component_slider.xml b/android-design-system/design-system-internal/src/main/res/layout/component_slider.xml
index 614632b312d8..413b92c6996d 100644
--- a/android-design-system/design-system-internal/src/main/res/layout/component_slider.xml
+++ b/android-design-system/design-system-internal/src/main/res/layout/component_slider.xml
@@ -26,6 +26,13 @@
android:layout_height="wrap_content"
app:primaryText="Slider" />
+
+
-
\ No newline at end of file
+
+
+
+
+
diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/compose/slider/DaxSlider.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/compose/slider/DaxSlider.kt
new file mode 100644
index 000000000000..8837a97de4f2
--- /dev/null
+++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/compose/slider/DaxSlider.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2026 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.duckduckgo.common.ui.compose.slider
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.duckduckgo.common.ui.compose.theme.DuckDuckGoTheme
+import com.duckduckgo.common.ui.compose.tools.PreviewBox
+import com.duckduckgo.mobile.android.R
+
+/**
+ * DuckDuckGo design system composable slider component.
+ *
+ * Wraps Material3 [Slider] with DuckDuckGo theme colors. Value labels, tick marks, stop indicators
+ * and the thumb press halo are not drawn.
+ *
+ * @param value the current value of the slider, coerced into [valueRange]
+ * @param onValueChange callback invoked continuously as the slider is dragged
+ * @param modifier the [Modifier] to apply
+ * @param enabled whether the slider is enabled
+ * @param valueRange the inclusive range of values this slider can take
+ * @param steps the number of discrete values between the ends of [valueRange], or 0 for a continuous slider
+ * @param onValueChangeFinished callback invoked when the drag gesture ends, for committing the settled value
+ * @param interactionSource the [MutableInteractionSource] representing the stream of interactions for this slider
+ *
+ * Asana Task: https://app.asana.com/1/137249556945/project/1202857801505092/task/1218311474562583?focus=true
+ * Figma reference: https://www.figma.com/design/BOHDESHODUXK7wSRNBOHdu/%F0%9F%A4%96-Android-Components?node-id=3116-4290&m=dev
+ */
+@Composable
+fun DaxSlider(
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ val colors = DaxSliderDefaults.colors()
+ Slider(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier.alpha(if (enabled) 1f else DaxSliderDefaults.DisabledAlpha),
+ enabled = enabled,
+ valueRange = valueRange,
+ steps = steps,
+ onValueChangeFinished = onValueChangeFinished,
+ interactionSource = interactionSource,
+ colors = colors,
+ thumb = {
+ Box(
+ modifier = Modifier
+ .size(DaxSliderDefaults.ThumbSize)
+ .clip(CircleShape)
+ .background(DuckDuckGoTheme.colors.brand.accentBlue),
+ )
+ },
+ track = { sliderState ->
+ SliderDefaults.Track(
+ sliderState = sliderState,
+ modifier = Modifier.height(DaxSliderDefaults.TrackHeight),
+ enabled = enabled,
+ colors = colors,
+ drawStopIndicator = null,
+ drawTick = { _, _ -> },
+ thumbTrackGapSize = 0.dp,
+ )
+ },
+ )
+}
+
+private object DaxSliderDefaults {
+
+ const val DisabledAlpha = 0.4f
+
+ val ThumbSize = 20.dp
+
+ val TrackHeight: Dp
+ @Composable
+ get() = dimensionResource(R.dimen.sliderTrackHeight)
+
+ @Composable
+ fun colors(): SliderColors = SliderDefaults.colors(
+ thumbColor = DuckDuckGoTheme.colors.brand.accentBlue,
+ activeTrackColor = DuckDuckGoTheme.colors.brand.accentBlue,
+ inactiveTrackColor = DuckDuckGoTheme.colors.system.sliderTrackInactive,
+ disabledThumbColor = DuckDuckGoTheme.colors.brand.accentBlue,
+ disabledActiveTrackColor = DuckDuckGoTheme.colors.brand.accentBlue,
+ disabledInactiveTrackColor = DuckDuckGoTheme.colors.system.sliderTrackInactive,
+ )
+}
+
+@PreviewLightDark
+@Composable
+private fun DaxSliderAllStatesPreview() {
+ PreviewBox {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ DaxSliderPreviewState(value = 0f)
+ DaxSliderPreviewState(value = 0.3f)
+ DaxSliderPreviewState(value = 1f)
+ DaxSliderPreviewState(value = 0.3f, enabled = false)
+ DaxSliderPreviewState(value = 100f, valueRange = 70f..170f, steps = 9)
+ }
+ }
+}
+
+@Composable
+private fun DaxSliderPreviewState(
+ value: Float,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ steps: Int = 0,
+) {
+ var sliderValue by remember { mutableFloatStateOf(value) }
+ DaxSlider(
+ value = sliderValue,
+ onValueChange = { sliderValue = it },
+ enabled = enabled,
+ valueRange = valueRange,
+ steps = steps,
+ )
+}
diff --git a/lint-rules/src/main/java/com/duckduckgo/lint/registry/DuckDuckGoIssueRegistry.kt b/lint-rules/src/main/java/com/duckduckgo/lint/registry/DuckDuckGoIssueRegistry.kt
index 4ed54fcf9876..1e46b01e0934 100644
--- a/lint-rules/src/main/java/com/duckduckgo/lint/registry/DuckDuckGoIssueRegistry.kt
+++ b/lint-rules/src/main/java/com/duckduckgo/lint/registry/DuckDuckGoIssueRegistry.kt
@@ -55,6 +55,7 @@ import com.duckduckgo.lint.ui.DaxListItemContentDetector.Companion.INVALID_DAX_L
import com.duckduckgo.lint.ui.DaxTextColorUsageDetector.Companion.INVALID_DAX_TEXT_COLOR_USAGE
import com.duckduckgo.lint.ui.NoRawM3AlertDialogUsageDetector.Companion.NO_RAW_M3_ALERT_DIALOG_USAGE
import com.duckduckgo.lint.ui.NoRawM3ButtonUsageDetector.Companion.NO_RAW_M3_BUTTON_USAGE
+import com.duckduckgo.lint.ui.NoRawM3SliderUsageDetector.Companion.NO_RAW_M3_SLIDER_USAGE
import com.duckduckgo.lint.ui.NoRawM3SnackbarUsageDetector.Companion.NO_RAW_M3_SNACKBAR_USAGE
import com.duckduckgo.lint.ui.NoRawM3SurfaceUsageDetector.Companion.NO_RAW_M3_SURFACE_USAGE
import com.duckduckgo.lint.ui.DaxTextFieldTrailingIconDetector.Companion.INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE
@@ -144,6 +145,7 @@ class DuckDuckGoIssueRegistry : IssueRegistry() {
NO_RAW_M3_ALERT_DIALOG_USAGE,
NO_RAW_M3_SURFACE_USAGE,
NO_RAW_M3_SNACKBAR_USAGE,
+ NO_RAW_M3_SLIDER_USAGE,
).plus(WebViewCompatApisUsageDetector.issues)
diff --git a/lint-rules/src/main/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetector.kt b/lint-rules/src/main/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetector.kt
new file mode 100644
index 000000000000..b8aa87ce1bf7
--- /dev/null
+++ b/lint-rules/src/main/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetector.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2026 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.lint.ui
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category.Companion.CUSTOM_LINT_CHECKS
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.TextFormat
+import org.jetbrains.uast.UCallExpression
+import java.util.EnumSet
+
+@Suppress("UnstableApiUsage")
+class NoRawM3SliderUsageDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return RawM3SliderCallHandler(context)
+ }
+
+ internal class RawM3SliderCallHandler(private val context: JavaContext) : UElementHandler() {
+
+ override fun visitCallExpression(node: UCallExpression) {
+ if (isInDesignSystemModule(context.project.name)) return
+
+ val methodName = node.methodName ?: return
+ if (methodName != RAW_M3_SLIDER_NAME) return
+
+ val resolved = node.resolve() ?: return
+ val qualifiedName = resolved.containingClass?.qualifiedName ?: return
+ if (qualifiedName.startsWith("androidx.compose.material3")) {
+ context.report(
+ issue = NO_RAW_M3_SLIDER_USAGE,
+ location = context.getLocation(node),
+ message = NO_RAW_M3_SLIDER_USAGE.getExplanation(TextFormat.RAW),
+ )
+ }
+ }
+
+ private fun isInDesignSystemModule(projectName: String): Boolean {
+ return projectName.contains("design-system")
+ }
+ }
+
+ companion object {
+ private const val RAW_M3_SLIDER_NAME = "Slider"
+
+ val NO_RAW_M3_SLIDER_USAGE = Issue
+ .create(
+ id = "NoRawM3SliderUsage",
+ briefDescription = "Use DaxSlider instead of raw Material3 Slider",
+ explanation = """
+ Use the DuckDuckGo design system `DaxSlider` (com.duckduckgo.common.ui.compose.slider.DaxSlider) \
+ instead of the raw Material3 `Slider` composable.
+
+ Raw M3 Slider bypasses the design system's color and dimension tokens, and draws value labels, \
+ tick marks and stop indicators that DuckDuckGo sliders do not use. Using `DaxSlider` ensures \
+ visual consistency across the app.
+ """.trimIndent(),
+ moreInfo = "",
+ category = CUSTOM_LINT_CHECKS,
+ priority = 6,
+ severity = Severity.ERROR,
+ androidSpecific = true,
+ implementation = Implementation(
+ NoRawM3SliderUsageDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
+ ),
+ )
+ }
+}
diff --git a/lint-rules/src/test/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetectorTest.kt b/lint-rules/src/test/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetectorTest.kt
new file mode 100644
index 000000000000..311e264792bf
--- /dev/null
+++ b/lint-rules/src/test/java/com/duckduckgo/lint/ui/NoRawM3SliderUsageDetectorTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2026 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.lint.ui
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import com.duckduckgo.lint.ui.NoRawM3SliderUsageDetector.Companion.NO_RAW_M3_SLIDER_USAGE
+import org.junit.Test
+
+class NoRawM3SliderUsageDetectorTest {
+
+ private val m3SliderStub = TestFiles.kotlin(
+ """
+ package androidx.compose.material3
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Slider(value: Float, onValueChange: (Float) -> Unit) {}
+ """.trimIndent()
+ ).indented()
+
+ private val composableStub = TestFiles.kotlin(
+ """
+ package androidx.compose.runtime
+
+ annotation class Composable
+ """.trimIndent()
+ ).indented()
+
+ @Test
+ fun whenRawM3SliderUsedThenError() {
+ lint()
+ .files(
+ TestFiles.kt(
+ """
+ package com.example.feature
+
+ import androidx.compose.material3.Slider
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyScreen() {
+ Slider(value = 0f, onValueChange = {})
+ }
+ """.trimIndent()
+ ).indented(),
+ m3SliderStub,
+ composableStub,
+ )
+ .allowCompilationErrors()
+ .issues(NO_RAW_M3_SLIDER_USAGE)
+ .run()
+ .expectErrorCount(1)
+ }
+
+ @Test
+ fun whenDaxSliderUsedThenNoError() {
+ lint()
+ .files(
+ TestFiles.kt(
+ """
+ package com.example.feature
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun DaxSlider(value: Float, onValueChange: (Float) -> Unit) {}
+
+ @Composable
+ fun MyScreen() {
+ DaxSlider(value = 0f, onValueChange = {})
+ }
+ """.trimIndent()
+ ).indented(),
+ composableStub,
+ )
+ .issues(NO_RAW_M3_SLIDER_USAGE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun whenNonM3FunctionNamedSliderThenNoError() {
+ lint()
+ .files(
+ TestFiles.kt(
+ """
+ package com.example.feature
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Slider(value: Float, onValueChange: (Float) -> Unit) {}
+
+ @Composable
+ fun MyScreen() {
+ Slider(value = 0f, onValueChange = {})
+ }
+ """.trimIndent()
+ ).indented(),
+ composableStub,
+ )
+ .issues(NO_RAW_M3_SLIDER_USAGE)
+ .run()
+ .expectClean()
+ }
+}