A horizontal slider — a filled track, a knob, and an optional percentage tooltip that rides above it.
⤷ Track, with Pressed and Disabled states and a switchable tooltip. Still to polish: the knob looks the same when Disabled, and focus is not specified.The preview is draggable — grab the knob or press anywhere on the track, the same continuous model the slot gives you in Figma. Used where a value is approximate rather than typed — amount estimation, settings, filters. Contexts are illustrative; final screens will reference actual GCash patterns.
⤷ Track, DraggableFill, KnobContainer, Knob, Tooltip, Label, PointerDecorator, Pointer and #percentage all follow the conventions settled across Modal, Voucher and List Item, hasTooltip uses lowercase boolean values, and the axis now gates the bubble alone.⤷ Track is what makes DraggableFill resizable on an instance — without the slot the fill would be locked at its authored width, and the value would have to go back to being a variant.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| State=Default | N/A | N/A | 365 × 26 | Fill #005CE5 on a #E5EBF4 track. The default the component ships with. |
| State=Pressed | N/A | N/A | 365 × 26 | Fill darkens to #2340A9, the track to #D7E0EF, and the knob's ring to #ADBDDC — the only state where the knob itself responds. |
| State=Disabled | N/A | N/A | 365 × 26 | Fill and tooltip both drop to #C2CFE5, with the tooltip switching to dark text so the value stays readable. |
| hasTooltip=true | N/A | N/A | default | Shows the bubble above the knob. It sits outside the component's 26px box by design. |
| hasTooltip=false | N/A | N/A | no bubble | Hides the bubble only. The knob stays, so the control still reads as draggable — it used to disappear with the tooltip, which left a handle-less bar. |
| Value | N/A | N/A | fill width | Not a property. The designer resizes DraggableFill inside the slot; the knob and tooltip ride its right edge. |
- Eleven variants became a continuous value. The old build enumerated the value at 10% steps — eleven variants for something that is not discrete. The value is now the width of
DraggableFill, resized directly on the instance, so any value is reachable rather than the nearest tenth. C1 · Layer Structure & Naming - The slot is what makes that work.
DraggableFillsits inside⤷ Trackbecause a nested frame's size is not editable on an instance unless it is exposed. Without the slot the fill would be locked at its authored width and the value would have to go back to being a variant — so the slot is load-bearing rather than indirection. C4 · Native Mappability - Pressed and Disabled states exist now. Neither was specified before. Pressed darkens the fill to
#2340A9, the track to#D7E0EFand the knob ring to#ADBDDC; Disabled drops the fill and tooltip to#C2CFE5. C5 · Interaction State Coverage - The tooltip can be switched off. It used to be always on with no way to hide it.
hasTooltipnow controls it, and the values are lowercasetrue/falseto match every other boolean in the system. C2 · Variant & Property Naming - A layer was named after its own instructions. The fill was called
Adjust this bsed on the length needed— an instruction to the designer, typo included, repeated across every variant. It is nowDraggableFill, which keeps the authoring hint without shipping a sentence into the layer tree. C1 · Layer Structure & Naming - The duplicate track is gone. A
BGrectangle sat behind a separateSlider Slot, both 365 × 10 and both full width. The slot now carries the track's own fill and radius and is named⤷ Track, which removes a layer and leaves one thing responsible for the rail. C1 · Layer Structure & Naming - The tooltip's internals were renamed.
label,pointer-decoratorandpointerwere lowercase and kebab-cased, and the knob itself was still Figma's defaultEllipse. They are nowLabel,PointerDecorator,PointerandKnob, insideKnobContainer. C1 · Layer Structure & Naming - The disabled tooltip went from unreadable to 9.27:1. It was white on
#C2CFE5— 1.57:1, effectively invisible. An intermediate fix used the pressed navy, which passed but made the disabled state louder than the enabled one and identical to Pressed. The answer landed on keeping the bubble at#C2CFE5and switching the text to#0A2757: 9.27:1, and the bubble still reads as muted because it matches the disabled fill. WCAG exempts inactive controls from contrast, so this was optional — but the tooltip displays a value, and an unreadable value is worse than no value. C3 · Token Coverage - The tooltip overflowing the component is intentional. Both tooltip variants are 26 tall while the bubble sits 23px above the top edge, so nothing reserves space for it. Confirmed as intended: consumers leave clearance above the slider rather than the component growing to contain it. C1 · Layer Structure & Naming
- hasTooltip stays a variant, and that is a Figma limit rather than a choice. A boolean component property cannot target a layer nested inside a slot, and
Tooltiplives insideDraggableFillinside⤷ Track. It sits there so it hugs the fill's right edge as the fill resizes — moving it out would break that. So the axis stays and the set holds six variants rather than three. Recorded here as a deliberate divergence between the Figma model and the native API, where the tooltip will simply be an optional parameter. C2 · Variant & Property Naming - hasTooltip gates the bubble and nothing else. It used to remove the knob as well —
DraggableFillhad no children at all in thefalsevariants, so what was left was a filled bar with no handle. That read as a progress indicator rather than a slider, overlapping Progress Bar, and the property name gave no hint that the handle went with it.KnobContaineris now present in all six variants, with the pressed ring carried through, so the axis does exactly what it says. C2 · Variant & Property Naming - The slider fills its container. The authored 365 is a fill width, not a fixed one, so the control stretches to whatever the screen gives it. That matters more here than on most components — a slider at a fixed width would leave the value's position meaningless on any other screen size. C4 · Native Mappability
- Give the disabled knob a treatment. The knob is identical in Default and Disabled — same
bg/color-bg-mainfill, sameborder/color-border-weakring — while the track and the tooltip both shift. Confirmed deliberate: those two carry the signal, and the thumb is left alone on purpose. Worth revisiting only if the slider ever appears without its tooltip in a disabled state, since the track alone is a subtler cue than the pair. State - Make the sample value match the fill.
#percentagereads "10%" whileDraggableFillis 62 of 365, about 17%. Harmless in isolation, but a component that ships showing one number at a different position teaches the wrong relationship to whoever copies it. Docs - Range, step, min and max belong to the native API. The May assessment asked for them as axes. They have no Figma representation — a designer resizes the fill and that is the whole model — so they should be specified in the native contract instead of being added to the component. Documented in the property mapping. Docs
- Expect a rename at the Code Connect boundary.
DraggableFillwas kept deliberately because it tells the team the frame is resizable. Both native APIs call this the active track — Compose hasactiveTrackColor, SwiftUI fills the portion below the value — so the mapping isDraggableFillto active track rather than a one-to-one name. Noted so it is not read as a mismatch later. Docs - Specify the accessible slider contract. A slider needs a role, a current value, a range and increment actions — none of which are visible in a Figma frame. Screen reader users adjust it with gestures rather than dragging, so the value has to be announced on change and the control has to accept increment and decrement without a drag. A11y
- Publish the token names once Dev Mode is read. Fills across the component resolve to library variables — verified on
#percentage— but the read-only tools return variable IDs rather than names, so the spec tables carry hex values only. Token
- Confirm focus is out of scope. v2.0.1: Applied — confirmed out of scope. The component is mobile only, so there is no focus state to specify and none is missing. Recorded here rather than left as an open question, because "no focus variant" reads as an omission until someone says otherwise. State
Component Playground
Layout
VStack
Typography
Colors
iOS — Swift Package Manager
.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")
Android — Gradle (Kotlin DSL)
implementation("com.eastblue.ds:slider:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.slider.*
Three properties, and the interesting one is the slot. The value is the whole reason ⤷ Track exists. A slider’s value is continuous, and Figma has no property type for that — so rather than enumerate it as eleven variants at 10% steps, the component makes the track a slot and lets DraggableFill be resized inside it. The slot is not content anyone swaps; it is a value control wearing a slot’s clothes. Natively it maps to the binding, which is where a value belongs, so the thing that looks least like an API in Figma is the one that maps most directly. range, step, min and max have no Figma form at all and are the native API’s own — they get no row here, because a row would imply Figma says something about them.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| State — Default, Disabled, Pressed | not a parameter — Pressed is the drag itself; Disabled is .disabled(true) | not a parameter — Disabled is enabled = false |
| hasTooltip — true, false | showsTooltip: Bool = true | showsTooltip: Boolean = true |
| ⤷ Track (slot) | not a slot — value: Binding<Double>, in: ClosedRange<Double> | not a slot — value: Float, onValueChange, valueRange |
EBSlider( value: $amount, in: 0...100, showsTooltip: true )
EBSlider( value = amount, onValueChange = { amount = it }, valueRange = 0f..100f, showsTooltip = true )
EBSlider( value: $amount, in: 0...100, showsTooltip: true ) .disabled(true)
EBSlider( value = amount, onValueChange = { amount = it }, valueRange = 0f..100f, showsTooltip = true, enabled = false )
EBSlider( value: $amount, in: 0...100, showsTooltip: true ) // Pressed is the drag — there is nothing to pass
EBSlider( value = amount, onValueChange = { amount = it }, valueRange = 0f..100f, showsTooltip = true ) // Pressed is the drag — there is nothing to pass
| Requirement | iOS | Android |
|---|---|---|
| Exposed as an adjustable control | .accessibilityValue plus .accessibilityAdjustableAction | Modifier.semantics { setProgress { … } } with ProgressBarRangeInfo |
| Adjustable without dragging | Swipe up and down increments and decrements | Accessibility actions for increase and decrease |
| Value announced on change | Announce the formatted value, not the raw number | Same — the tooltip's format is the announced string |
| Tooltip is not a separate element | Hidden from the tree; its value lives on the slider | contentDescription = null; value carried by the slider |
| Knob meets the touch minimum | 16 × 16 needs a 44 × 44 hit area around it | 16 × 16 needs 48dp |
Do
Use a slider where the value is approximate — estimation, filtering, settings.
Don't
Don't use one where an exact figure matters; a field is faster and more accurate.
Do
Leave 23px of clearance above when the tooltip is on.
Don't
Don't place it directly under other content — the bubble sits outside the component's box.
Do
Let the slider fill the width it is given.
Don't
Don't fix its width — the value's position stops meaning anything at another screen size.
Do
Switch the tooltip off when a nearby label already shows the value.
Don't
Don't show the same number twice.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | The instruction-as-layer-name is gone, the duplicate track rectangle removed, and the tooltip internals plus the knob renamed. The tooltip overflowing the frame is confirmed intentional. |
| C2 | Variant & Property Naming | Ready | State and hasTooltip with lowercase boolean values. The tooltip axis stays a variant because a boolean cannot reach a layer inside a slot — a Figma limit, documented rather than worked around — and it now gates the bubble alone. |
| C3 | Token Coverage | Ready | Every colour resolves to a system token, and the nine on the page are exactly the nine the file’s selection list carries. Five of the six roles move with State; only the knob’s fill holds still. One naming oddity is recorded rather than raised: Pressed paints bg/color-bg-primary-hover, the next step above bg/color-bg-primary, because no pressed token exists yet. |
| C4 | Native Mappability | Ready | A continuous value, a track, a thumb and an optional bubble map onto both platforms’ slider primitives. The slot is the one thing that looks unusual in Figma and is the most ordinary natively: it exists because Figma cannot express a continuous value, and it maps to the binding. range, step, min and max have no Figma form and belong to the native API. |
| C5 | Interaction State Coverage | Ready | Default, Pressed and Disabled all exist, and the two things this criterion used to hang on are decisions rather than gaps. The knob is deliberately unchanged in Disabled — the track and the tooltip both shift, and that carries the signal without touching the thumb. Focus is out of scope: the component is mobile only. Pressed is the one state that reaches the knob, giving it a border/color-border-strong ring. |
| C6 | Asset & Icon Quality | Ready | No imported artwork — the knob is a vector ellipse and the tooltip pointer a vector frame. |
3 State values × 2 hasTooltip values = 6 variants. The value is not an axis — it is the width of DraggableFill, resized through the slot.
| State | hasTooltip | Track | Fill | Node |
|---|---|---|---|---|
| Default | true | #E5EBF4 | #005CE5 | 6764:106099 |
| Default | false | #E5EBF4 | #005CE5 | 6802:105581 |
| Disabled | true | #E5EBF4 | #C2CFE5 | 6802:105593 |
| Disabled | false | #E5EBF4 | #C2CFE5 | 7085:108656 |
| Pressed | true | #D7E0EF | #2340A9 | 7085:108663 |
| Pressed | false | #D7E0EF | #2340A9 | 7085:108670 |
bg/color-bg-main fill, same border/color-border-weak ring — while the track and the tooltip both shift. Confirmed that those two carry the signal and the thumb is left alone on purpose. It stays as a recommendation to revisit only if a disabled slider ever ships without its tooltip, since the track alone is the subtler cue.State is the driving property, so Default, Disabled and Pressed each get a card and hasTooltip becomes the only control.State — track, fill, knob ring, tooltip and its label — and only the knob’s fill holds still. A Colors by State table now carries all fifteen values against the nine tokens the file actually uses. The preview already had every one of them right; it was the page that only described one column.dy 2, blur 2, black at 10% — an effect that appears in neither the layer tree nor get_node_info, only in the SVG export’s filter. Third artwork detail this batch that the node tree does not expose..eb-preview-sldr declared font-family: inherit, which resolves to BarkAda. The one text layer is Primary/Label/Small, so the root names Proxima Soft.DraggableFill width is the value, — no Figma equivalent was a placeholder for range and step, and one row bundled ⤷ Track with DraggableFill. Three rows now, matching the panel exactly. Range, step, min and max get no row at all: they have no Figma form, and a row would imply Figma says something about them.DraggableFill inside it. It is not content anyone swaps — it is a value control wearing a slot’s clothes, and natively it maps straight onto the binding. The thing that looks least like an API in Figma turns out to map most directly.gcash/east-blue-ios and com.gcash.eastblue:components:1.0.0, with no Import line. Slider has no navGroup, so it is a family of one and the artifact takes its slug: com.eastblue.ds:slider:1.0.0.padding-top. Corrected, and worth naming as a class of error: a number that only ever existed in the documentation, handed to developers as though the component said it.State now, including an honest Pressed — no call of its own: two of the three states have no API expression, and saying so beats inventing one.PropertyMapping.astro, which repaired the intro on 17 component pages — twelve of them broken long before this review. Nothing in any data file changed: writing <code> there was always the convention, and the renderer simply was not honouring it.DraggableFill, resized on the instance through ⤷ Track, which is what the first assessment asked for.hasTooltip added so the bubble can be switched off, with lowercase true / false values.Adjust this bsed on the length needed — an instruction with a typo, repeated across all six variants — renamed to DraggableFill.BG rectangle removed; the slot now carries the rail's own fill and radius as ⤷ Track.label, pointer-decorator and pointer renamed to PascalCase, and the knob's Ellipse renamed to Knob inside KnobContainer.#0A2757, rather than saturating the bubble and making the disabled state louder than the enabled one.hasTooltip kept as a variant axis — a boolean property cannot target a layer inside a slot, and the tooltip sits there so it tracks the fill's edge. Recorded as a Figma-versus-code divergence.hasTooltip narrowed to the bubble alone — setting it to false used to remove KnobContainer as well, leaving a handle-less bar that read as a progress indicator and overlapped Progress Bar. The knob is now present in all six variants, pressed ring included.3235:60722 (Sticker Sheets v2) to 6802:105580 (2026 Working File).EBSlider primitive with value: Double + range + step. Add Pressed / Focused / Disabled state coverage. Open