A search input field with a leading magnifying-glass icon and an optional clear button.
4697:18836 in the 2026 Working File and confirmed a standalone primitive. State = Default | Focused | Error | Disabled, the leading glyph is a library icon instance, the trailing slot holds real Close / Error icons and appears only where it does something, every layer carries a semantic name, and colors come from the shared generic token scale. The banded top/bottom border and the constant container chrome are both intentional — this is a full-width element above content, not a field inside a form stack. Error on the State axis is now a documented system-wide exception recorded in §6 of the Property Naming Guidelines rather than a per-component argument, and the filled-but-unfocused state is derived from value presence in code instead of doubling the variant set. Native search semantics, the keyboard contract and the accessibility requirements are all documented. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Contexts are illustrative. Final screens will reference actual GCash patterns.
TrailingIcon holds real Close / Error icon instances, and all four interaction states carry their own label and glyph colors. Nothing external required to render.State = Default | Focused | Error | Disabled, matching the sibling fields; every layer carries a semantic name (Container · Value · TrailingIcon); colors come from the shared generic token scale rather than a component-scoped namespace. Error on the State axis is a documented, deliberate exception.TrailingIcon is a real slot carrying swappable Close and Error instances. The leading glyph is deliberately not slotted — a search field’s search icon is part of what identifies the component, and making it swappable would turn this into a generic icon-prefixed input, which is a different component’s job.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default (empty) | Yes | Yes | State=Default | Placeholder label at #90A8D0, no trailing icon — nothing to clear in the empty state. |
| Focused | Yes | Yes | State=Focused | Value at #0A2757 with a caret. Container chrome is unchanged from Default — focus is conveyed by content alone, not by a ring or border shift. |
| Error | Yes | Yes | State=Error | Value at #0A2757, trailing slot swaps to the red Error icon. The container border does not change color. |
| Disabled | Yes | Yes | State=Disabled | Label and leading glyph mute to #C2CFE5, no trailing icon. |
- Code Connect mappings not registered. Blocked — no native library exists yet. The structural blockers are cleared: the property schema is a clean
Stateenum and every layer is semantically named. C7 · Code Connect Linkability
- State coverage completed. v2.0: Rebuilt on node
4697:18836.State = Default | Focused | Error | Disablednow ships all four interaction states, matching the schema every sibling field uses. Replaces the old two-valuedefault/filledaxis. (C5) C5 · Interaction State Coverage - Leading search glyph is now a vector instance. v2.0: The raster
<img>was replaced with aSearch Smallicon instance, restoring token-based recoloring and crisp rendering at any density. Applied as recommended. (C6 · Asset) C6 · Asset & Icon Quality - Trailing slot holds real icons. v2.0: The
Placeholderscaffolding wrapper is gone.TrailingIconnow carries aCloseinstance in Default, Focused and Disabled, and anErrorinstance in Error. Applied as recommended. (C1 · Slot) C1 · Layer Structure & Naming - State axis no longer conflates content with interaction. v2.0:
filledwas dropped from the axis rather than split into a boolean — content-filled is derived from whether a value is present, and the axis now carries interaction states only. Resolves the C2 finding, though by a different route than the recommendedisFilledsplit. (C2) C2 · Variant & Property Naming - Layer naming cleaned up. v2.1:
search-field→Container,#search→Value,icon-container→TrailingIcon, across all four variants, and the Error variant's placeholder junk text (@@ . @#$!# #_12) was replaced with realistic sample content. (C1) C1 · Layer Structure & Naming - Clear button removed from the empty state. v2.2:
TrailingIconwas deleted fromState=Default, so the empty field no longer offers a clear affordance with nothing to clear.Valuewidened from 250 to 282 to take the freed space. The trailing slot now appears only where it does something —Closein Focused,Errorin Error. (C4 · Slot) C4 · Native Mappability - Disabled state's trailing icon resolved. v2.2: Rather than muting the
Closeicon,TrailingIconwas removed fromState=Disabledaltogether — a disabled field has nothing to clear either. Consistent with the Default fix, and it removes the full-strength blue affordance that had read as tappable. (C5 · State) C5 · Interaction State Coverage - Banded border confirmed intentional. v2.3: Closed by owner decision — not a divergence. Search Field is a full-width element that sits above other content rather than inside a form stack, so the top/bottom rule is the correct chrome for that role; a rounded-rect stroke would imply an inline form field it isn't. The Form Elements siblings use a rounded rect because they sit within forms. Native implementations should render a full-bleed container with top and bottom dividers, not a bordered text field. (C4 · Family) C4 · Native Mappability
-
State=Errorexception documented. v2.3: Closed on owner confirmation —Errorstays on theStateaxis as a deliberate, documented exception to the State/Status rule, matching how most design systems model form-field validation. Keeps the set at 4 variants instead of the 6 a split would require. Recorded on the owner's word; component property descriptions are not readable from the assessment tooling. (C2 · Property) C2 · Variant & Property Naming - Token namespace resolved — reverted to generic tokens. v2.3: Closed by owner decision. The component-scoped
main/search/color/default/*namespace was abandoned in favour of the shared generic tokens, so the single-sub-mode problem disappears — there is no longer a per-component token tier that has to enumerate every state. Colors now inherit from the system scale like the rest of Form Elements. (Token) C3 · Token Coverage - Confirmed as a standalone primitive. v2.3: Closed by owner decision — Search Field stays its own component rather than folding into Input Field as a composed variant. The full-width, above-content role and its banded chrome are genuinely distinct from a field inside a form stack, and merging would force the banded treatment to become a variant of a rounded-rect primitive. This also settles the token direction:
main/search/*is expanded, not retired. (Composition) C4 · Native Mappability - Search icon delegated to the iconography team. v2.3: Removed from this component's scope. The
Search Smallglyph is an instance of the shared icon library — the correct setup, so edits to the icon propagate everywhere it is used. Theshape_fullBOOLEAN_OPERATION lives in the library source component (4629:59009), not in Search Field, so flattening it is the icon owner's call and affects every consumer equally. No action here. (C6) C6 · Asset & Icon Quality - Filled-but-unfocused state documented rather than added as a variant. v2.4: The clear affordance is driven by value presence in code, not by a Figma variant.
Statestays a pure interaction axis of four; a field holding a value while unfocused renders asDefaultchrome with theValuecolor ofFocused(#0A2757) and the trailingCloseicon shown. Adding ahasValueboolean would have doubled the set to depict something the runtime already derives from whether the text is empty. Native implementations should show the clear control whenever the bound text is non-empty, regardless of focus. (State · Docs) C5 · Interaction State Coverage - State / Status exception recorded at family level. v2.4: Written into §6 of the Property Naming Guidelines rather than re-argued per component. Text-entry components may carry
Erroron theStateaxis; everything else keepsState(interaction) andStatus(system report) on separate axes. The entry names the three reasons — a field in error is in a distinct interaction state, splitting multiplies variants for a combination that does not occur at design time, and every major system models validation this way — and lists the components covered: Search Field, Amount Text Field, Text Area and future text-entry siblings. Amount Text Field had already settled the same question on its own page; the guideline entry now carries it once so no third component has to. (Family · Docs) C2 · Variant & Property Naming - Native search semantics documented. v2.4: The two platforms model search differently and neither maps to a plain text field. iOS:
.searchable(text:)is a modifier applied to aNavigationStackorList, not a standalone view — the system owns placement, the cancel button and the scroll-to-reveal behavior, so a hand-built field should not be substituted. Android: Material 3 offersSearchBar, which expands into a full-screen search surface with its own result list, or a plainTextFieldwith a leading search icon where inline search is wanted. Search Field’s banded full-width chrome maps to the latter. Keyboard contract: Enter submits (onSubmit/ImeAction.Search), Escape or the clear control empties the field and returns focus to it, and the field never submits on every keystroke unless the screen is explicitly live-filtering. (Docs) C4 · Native Mappability - Search and clear-button accessibility documented. v2.4: The field must announce as a search input rather than a generic text field —
.accessibilityAddTraits(.isSearchField)on iOS,Modifier.semantics { role = Role.SearchField }or the equivalentcontentDescriptionon Android — so VoiceOver and TalkBack read it correctly and users can jump to it by type. The trailing control needs its own label, “Clear search”, not the icon name; it is a button, not decoration, and must be reachable and at least 44×44pt / 48×48dp as a touch target even though the glyph is 24×24. InDisabledthe field should be announced as dimmed rather than hidden from the tree. (A11y) C5 · Interaction State Coverage - Container chrome confirmed constant across states. v2.4: The border stays
#E5EBF4in all four states — focus is carried by the caret and the clear control, error by the trailing icon. Confirmed intentional: Search Field is a full-width band above content rather than a field inside a form, so a focus ring or an error-colored border would read as an inline form field it is not. Native implementations should follow suit and convey focus and error through content rather than the container. Recorded because a reviewer comparing this to the Form Elements siblings will notice the difference and should find the reasoning rather than re-open it. (C5 · Docs) C5 · Interaction State Coverage
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 | Ready | Container · Value · TrailingIcon — every layer semantically named, scaffolding removed. |
| C2 | Variant & Property Naming | Needs Refinement | State = Default | Focused | Error | Disabled matches the sibling axis. Remaining: Error is a Status value on the State axis. |
| 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 | Needs Refinement | Banded border is intentional — render as a full-bleed container with top/bottom dividers, not a bordered text field. Remaining: role=search semantics undocumented. |
| C5 | Interaction State Coverage | Needs Refinement | All four states ship, each with correct trailing affordance. Remaining: filled-but-unfocused isn't modeled. |
| C6 | Asset & Icon Quality | Ready | Leading glyph is a Search Small instance from the shared icon library, so icon edits propagate. The shape_full boolean op lives in the library source, outside this component's scope. |
| C7 | Code Connect Linkability | Not Mapped | Blocked — no native library exists yet. The structural blockers (C1/C5/C6) are now cleared. |
| Aspect | Status | Notes |
|---|---|---|
| Property naming | Needs Refinement | State enum maps cleanly. Open question: whether Error belongs on a separate Status axis |
| State coverage | Ready | Default / Focused / Error / Disabled all ship |
| Icon quality | Needs Refinement | Vector leading glyph and real trailing icons; shape_full boolean op remains |
| 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