A search input field with a leading magnifying-glass icon and an optional clear button.
img, trailing slot is an unresolved Placeholder circle, and the banded top/bottom border diverges from the rest of the Form Elements family. Consider composing from Input Field + leading/trailing icon slots instead of shipping a bespoke component.Contexts are illustrative. Final screens will reference actual GCash patterns.
icon-container holds an unresolved Placeholder wrapper with a raw icon-placeholder circle — consumers must instance-swap to get a usable clear/cancel affordance. Missing focused, error, and disabled tokens.state=default/filled — conflates "has content" with the four interaction states used by every sibling field (Default/Active/Error/Disabled). Token namespace (main/search/*) isolates this from the shared field/* tokens other siblings rely on.icon-container is a real slot and accepts a swap (swapIcon). Leading icon is not slotted — locked to the bundled search glyph (which is raster, not vector).| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default (empty) | Yes | Yes | state=default | Placeholder text at 50% opacity, trailing slot holds placeholder circle. |
| Filled (has query) | Yes | Yes | state=filled | Text at full opacity (#0A2757), identical container, trailing slot unchanged. |
| Focused | No | No | — | No visible focused variant. Native focus ring cannot be approximated from DS. |
| Error | No | No | — | No error state defined. |
| Disabled | No | No | — | No disabled state defined. |
- State coverage is incomplete. Only
defaultandfilledare shipped; focused, error, and disabled are absent. NativeTextField/SearchBarexpect all four interaction states for focus rings, validation, and disabled styling. C5 · Interaction State Coverage - Leading search glyph is a raster asset. The search icon renders via
<img src={imgShapeFull}>(raster PNG) rather than a vector instance from the icon library. Blocks token-based recoloring and fails crisp rendering on high-density displays. C6 · Asset & Icon Quality - Trailing slot ships a
Placeholderwrapper with a raw circle.icon-containercontains aPlaceholderframe wrapping anicon-placeholderpink-circle shape. This is authoring scaffolding that should be replaced with a real clear/cancel icon (or removed) before the component leaves design. C1 · Layer Structure & Naming -
statevariant axis conflates content and interaction.state=default/filledis a derived content signal (has a value or not) — it shouldn't occupy the same axis that other Form Elements reserve for Default/Active/Error/Disabled. Also fails Code Connect's expectation of boolean-like or enum-of-states schemas. C2 · Variant & Property Naming - Banded border diverges from the Form Elements family. Container uses
border-top + border-bottomonly, no left/right,radius-0. Every sibling (Input, Labeled, Select, Recipient, View Only) uses a full rounded-rect stroke at 6px radius. No native primitive renders this shape by default — forces custom background work on both platforms. C4 · Native Mappability - Code Connect mappings not registered. Structural gaps (C1/C2/C5/C6) must be resolved before linking. No native file exists yet. C7 · Code Connect Linkability
- Compose from Input Field instead of shipping a parallel primitive. Once Input Field gains
leadingIcon/trailingIconslots (already recommended in its assessment), a Search Field becomes Input Field + search glyph leading + clear-button trailing — no new component needed. Retiresmain/search/*tokens and inherits Default/Active/Error/Disabled for free. Composition - Swap the raster
shape_fullfor the canonical search icon instance. Reference the same vector icon used elsewhere (24px Search Small) so it inheritsmain/{component}/color/icon-leadingrecoloring across modes. Asset - Replace the trailing
Placeholderwrapper with a real Clear (X) icon instance. The currentPlaceholder > container > icon-placeholderpath is authoring scaffolding. Bind to a 24px Close / Clear icon and expose it as an optional slot that hides whenstate=default. Slot - Add Active, Error, and Disabled variants, and split content-filled from interaction state. Adopt the sibling schema:
State = Default | Active | Error | Disabledplus a booleanisFilled = true/false. That yields 8 variants and matches Input Field's axis model exactly. State - Align the border treatment with the Form Elements family. If Search Field remains a standalone component, switch to the shared 6px rounded-rect stroke — the banded top/bottom look is a screen-level pattern (section divider), not a field chrome treatment, and can be added by the surrounding layout. Family
- Rename the
main/search/color/default/*namespace to match the new schema. Either retire tofield/*(if composed) or expand tomain/search/color/{default|active|error|disabled}/*so tokens cover every state. Remove the/default/sub-mode once other states exist. Token - Document search semantics for native handoff. iOS uses
.searchable(text:)on a container (it is not a standalone view); Android uses Material 3SearchBar(which expands into full-screen search) or aTextFieldwith a leading search icon. Document both paths and the Enter-to-submit / Escape-to-clear keyboard contract. Docs - Add
role="search"/ search semantics and a labeled clear button. The clear-button slot needs its own accessibility label ("Clear search"). iOS VoiceOver and Android TalkBack must announce the field as a search input, not a generic text field. A11y
Empty state. Placeholder label at 50% opacity (#90A8D0), leading search glyph at 80% opacity.
Only a single variable mode (default) is bound on main/search/*. Focused, error, and disabled tokens do not exist yet.
| Role | Token | DEFAULT | FILLED |
|---|---|---|---|
| Background | main/search/color/default/bg | #FFFFFF | #FFFFFF |
| Border (top + bottom) | main/search/color/default/border | #F6F9FD (80%) | #F6F9FD (80%) |
| Placeholder | main/search/color/default/placeholder | #90A8D0 (50%) | – |
| Text | main/search/color/default/text | – | #0A2757 |
| Icon (leading) | main/search/color/default/icon-leading | #6780A9 (80%) | #6780A9 (80%) |
| Icon (trailing) | main/search/color/default/icon-trailing | #6780A9 | #6780A9 |
State shown when a query has been entered. Text uses #0A2757 at full opacity.
| Role | Token | Token |
|---|---|---|
| Container size | 360 × 56 px | — |
| Padding (horizontal) | 22 px left / 24 px right | — / space/space-24 |
| Padding (vertical) | 16 px | space/space-16 |
| Gap (icon ↔ text) | 8 px | space/space-8 |
| Gap (trailing slot) | 12 px | space/space-12 |
| Corner radius | 0 | radius/radius-0 |
| Border | 1 px top + bottom only | — |
| Leading icon size | 24 × 24 px | — |
| Trailing slot size | 24 × 24 px | — |
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios"
Android — Gradle (Kotlin DSL)
dependencies { implementation("com.eastblue.ds:form-elements:1.0.0") }
Import
import EastBlueDS // SwiftUI import com.eastblue.ds.form.* // Compose
Package not yet published. These are the planned distribution paths.
| Figma Property | SwiftUI | Compose |
|---|---|---|
| state = default / filled | text: Binding<String> | query: String |
| — (missing) | .focused() / @FocusState | interactionSource |
| — (missing) | .disabled(true) | enabled = false |
| swapIcon (trailing) | trailingIcon: Image? | trailingIcon: @Composable |
| label | prompt: Text | placeholder: String |
EBSearchField("Search", text: $query, onSubmit: { runSearch(query) }, onClear: { query = "" })
EBSearchField( query = query, onQueryChange = { query = it }, onSearch = { runSearch(query) }, placeholder = "Search" )
NavigationStack { List(results) { row in Text(row.title) } } .searchable(text: $query, prompt: "Search")
SearchBar( query = query, onQueryChange = { query = it }, onSearch = { runSearch(query) }, active = active, onActiveChange = { active = it }, placeholder = { Text("Search") }, leadingIcon = { Icon(Icons.Default.Search, null) } ) { /* results */ }
EBInputField("Search", text: $query) .ebLeadingIcon(Image("search")) .ebTrailingIcon(query.isEmpty ? nil : Image("close")) { query = "" } .ebRole(.search)
EBInputField( value = query, onValueChange = { query = it }, placeholder = "Search", leadingIcon = { Icon(Icons.Default.Search, null) }, trailingIcon = if (query.isNotEmpty()) { { IconButton({ query = "" }) { Icon(Icons.Default.Close, "Clear search") } } } else null )
| Requirement | iOS | Android |
|---|---|---|
| Minimum touch target | 44 × 44 pt (container is 56pt ✓) | 48 × 48 dp (container is 56dp ✓) |
| Search role / trait | .searchable or .accessibilityAddTraits(.isSearchField) | SearchBar sets role automatically, else semantics { role = Role.TextField; contentType = ContentType.SearchQuery } |
| Clear button label | .accessibilityLabel("Clear search") | contentDescription = "Clear search" |
| Submit / Enter | .submitLabel(.search) + onSubmit | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search) |
| Escape to clear | Hardware keyboard: handle in onKeyPress(.escape) | Handle in onKeyEvent for keyboard users |
Do
Use Search Field for free-text query input that filters or retrieves results. Show the clear (X) button only when the field has content.
Don't
Use Search Field for destinations that don't actually filter or search. Use Input Field for generic text entry.
Do
Pair with a results region below the field and announce result counts to assistive tech when the query updates.
Don't
Ship the placeholder circle in the trailing slot — always swap to a real Clear / Cancel icon before publishing a screen.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Requires Rework | Trailing Placeholder > container > icon-placeholder chain is authoring scaffolding, not a named icon. |
| C2 | Variant & Property Naming | Requires Rework | state=default/filled conflates content with interaction. Diverges from sibling State axis (Default/Active/Error/Disabled). |
| C3 | Token Coverage | Needs Refinement | All visible colors bound to main/search/color/default/*, but only a default sub-mode exists — no tokens for focused/error/disabled. |
| C4 | Native Mappability | Requires Rework | Top+bottom banded border isn't a native default. Missing role=search semantics in layer model. |
| C5 | Interaction State Coverage | Requires Rework | Only default/filled. No focused, error, or disabled variants. |
| C6 | Asset & Icon Quality | Requires Rework | Leading search glyph is a raster img (shape_full), not a vector instance. |
| C7 | Code Connect Linkability | Not Mapped | Blocked by C1/C2/C5/C6. No CLI mappings registered. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Requires Rework | state=default/filled axis needs split into State (enum) + isFilled (bool) |
| State coverage | Requires Rework | Missing Active / Error / Disabled |
| Icon quality | Requires Rework | Raster leading glyph + placeholder trailing slot |
| Native component file | Not Mapped | EBSearchField.swift / EBSearchField.kt not yet created |
A single state axis with two values. Both variants are 360 × 56 px.
| state | Dimensions | Node ID |
|---|---|---|
| default | 360 × 56 | 50:78118 |
| filled | 360 × 56 | 50:78126 |
state=default/filled). Part of Form Elements group. Verdict: Restructure / Requires Rework.
Documentedshape_full rendered via img, not a vector instance.
OpenPlaceholder > icon-placeholder circle rather than a real Clear icon.
Openstate axis conflates content and interaction — default/filled is a derived content signal, not a state-machine value.
Openradius-0. Siblings use full rounded-rect stroke at 6px.
Open