diff --git a/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json b/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json
new file mode 100644
index 0000000000..878a54a550
--- /dev/null
+++ b/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: add OK search-input",
+ "packageName": "@ni/ok-angular",
+ "email": "1458528+fredvisser@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json b/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json
new file mode 100644
index 0000000000..719a26842e
--- /dev/null
+++ b/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: add OK search-input",
+ "packageName": "@ni/ok-components",
+ "email": "1458528+fredvisser@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/angular-workspace/example-client-app/src/app/app.module.ts b/packages/angular-workspace/example-client-app/src/app/app.module.ts
index 397d5a7efe..9a08cbd981 100644
--- a/packages/angular-workspace/example-client-app/src/app/app.module.ts
+++ b/packages/angular-workspace/example-client-app/src/app/app.module.ts
@@ -35,6 +35,7 @@ import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text/viewer'
import { NimbleRichTextEditorModule } from '@ni/nimble-angular/rich-text/editor';
import { NimbleRichTextMentionUsersModule } from '@ni/nimble-angular/rich-text-mention/users';
import { OkButtonModule } from 'ok-angular/button/ok-button.module';
+import { OkSearchInputModule } from 'ok-angular/search-input/ok-search-input.module';
import { SprightChatConversationModule } from '@ni/spright-angular/chat/conversation';
import { SprightChatInputModule } from '@ni/spright-angular/chat/input';
import { SprightIconWorkItemCalendarWeekDirective } from '@ni/spright-angular/icons/work-item-calendar-week';
@@ -121,6 +122,7 @@ import { CustomAppComponent } from './customapp/customapp.component';
NimbleIconPencilToRectangleModule,
NimbleIconMessagesSparkleModule,
OkButtonModule,
+ OkSearchInputModule,
SprightChatConversationModule,
SprightChatInputModule,
SprightChatMessageInboundModule,
diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html b/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html
index 4458204fde..b906de9546 100644
--- a/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html
+++ b/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html
@@ -519,5 +519,9 @@
Button (Ok)
Ok
+
diff --git a/packages/angular-workspace/ok-angular/search-input/ng-package.json b/packages/angular-workspace/ok-angular/search-input/ng-package.json
new file mode 100644
index 0000000000..0cd0286f2e
--- /dev/null
+++ b/packages/angular-workspace/ok-angular/search-input/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts b/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts
new file mode 100644
index 0000000000..3fba48e454
--- /dev/null
+++ b/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts
@@ -0,0 +1,36 @@
+import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
+import type { SearchInput } from '@ni/ok-components/dist/esm/search-input';
+import { searchInputTag } from '@ni/ok-components/dist/esm/search-input';
+import type { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types';
+
+export type { SearchInput };
+export { searchInputTag };
+
+/**
+ * Directive to provide Angular integration for the search input.
+ */
+@Directive({
+ selector: 'ok-search-input',
+ standalone: false
+})
+export class OkSearchInputDirective {
+ @Input()
+ public set appearance(value: SearchInputAppearance) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value);
+ }
+
+ @Input()
+ public set placeholder(value: string) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'placeholder', value);
+ }
+
+ @Input()
+ public set value(value: string) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'value', value);
+ }
+
+ public constructor(
+ private readonly elementRef: ElementRef,
+ private readonly renderer: Renderer2
+ ) {}
+}
\ No newline at end of file
diff --git a/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts b/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts
new file mode 100644
index 0000000000..2eb0b5b0a5
--- /dev/null
+++ b/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts
@@ -0,0 +1,12 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { OkSearchInputDirective } from './ok-search-input.directive';
+
+import '@ni/ok-components/dist/esm/search-input';
+
+@NgModule({
+ declarations: [OkSearchInputDirective],
+ imports: [CommonModule],
+ exports: [OkSearchInputDirective]
+})
+export class OkSearchInputModule { }
\ No newline at end of file
diff --git a/packages/angular-workspace/ok-angular/search-input/public-api.ts b/packages/angular-workspace/ok-angular/search-input/public-api.ts
new file mode 100644
index 0000000000..98cadeb10c
--- /dev/null
+++ b/packages/angular-workspace/ok-angular/search-input/public-api.ts
@@ -0,0 +1,2 @@
+export * from './ok-search-input.directive';
+export * from './ok-search-input.module';
\ No newline at end of file
diff --git a/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts b/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts
new file mode 100644
index 0000000000..9eb87457db
--- /dev/null
+++ b/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts
@@ -0,0 +1,14 @@
+import { TestBed } from '@angular/core/testing';
+import { OkSearchInputModule } from '../ok-search-input.module';
+
+describe('Ok search input', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [OkSearchInputModule]
+ });
+ });
+
+ it('custom element is defined', () => {
+ expect(customElements.get('ok-search-input')).not.toBeUndefined();
+ });
+});
\ No newline at end of file
diff --git a/packages/ok-components/src/all-components.ts b/packages/ok-components/src/all-components.ts
index a07b59ba2d..7853556071 100644
--- a/packages/ok-components/src/all-components.ts
+++ b/packages/ok-components/src/all-components.ts
@@ -8,3 +8,4 @@ import '@ni/spright-components/dist/esm/all-components';
import './button';
import './icon-dynamic';
+import './search-input';
diff --git a/packages/ok-components/src/search-input/index.ts b/packages/ok-components/src/search-input/index.ts
new file mode 100644
index 0000000000..38f9c0a4a5
--- /dev/null
+++ b/packages/ok-components/src/search-input/index.ts
@@ -0,0 +1,64 @@
+import { attr } from '@ni/fast-element';
+import { DesignSystem, FoundationElement } from '@ni/fast-foundation';
+import { styles } from './styles';
+import { template } from './template';
+import {
+ SearchInputAppearance,
+ type SearchInputAppearance as SearchInputAppearanceType
+} from './types';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ok-search-input': SearchInput;
+ }
+}
+
+/**
+ * A compact search input with a built-in clear affordance.
+ */
+export class SearchInput extends FoundationElement {
+ @attr
+ public placeholder = 'Search';
+
+ @attr
+ public value = '';
+
+ @attr
+ public appearance: SearchInputAppearanceType = SearchInputAppearance.outline;
+
+ private inputElement: HTMLInputElement | null = null;
+
+ public handleInput(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this.value = input.value;
+ }
+
+ public handleChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this.value = input.value;
+ }
+
+ public captureInputRef(element: HTMLInputElement | null): void {
+ this.inputElement = element;
+ }
+
+ public clear(): void {
+ if (this.value === '') {
+ return;
+ }
+
+ this.value = '';
+ this.inputElement?.focus();
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
+ }
+}
+
+const okSearchInput = SearchInput.compose({
+ baseName: 'search-input',
+ template,
+ styles
+});
+
+DesignSystem.getOrCreate().withPrefix('ok').register(okSearchInput());
+export const searchInputTag = 'ok-search-input';
\ No newline at end of file
diff --git a/packages/ok-components/src/search-input/styles.ts b/packages/ok-components/src/search-input/styles.ts
new file mode 100644
index 0000000000..5646d542c9
--- /dev/null
+++ b/packages/ok-components/src/search-input/styles.ts
@@ -0,0 +1,161 @@
+import { css } from '@ni/fast-element';
+import {
+ bodyFont,
+ bodyFontColor,
+ borderHoverColor,
+ borderRgbPartialColor,
+ borderWidth,
+ controlHeight,
+ fillHoverColor,
+ iconColor,
+ placeholderFontColor,
+ smallDelay,
+ standardPadding
+} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens';
+import { display } from '../utilities/style/display';
+
+export const styles = css`
+ ${display('inline-block')}
+
+ :host {
+ --ok-search-input-height: ${controlHeight};
+ --ok-search-input-inline-padding: ${standardPadding};
+ --ok-search-input-leading-space: calc(var(--ok-search-input-inline-padding) + 16px);
+ --ok-search-input-trailing-space: calc(var(--ok-search-input-inline-padding) + 20px);
+ --ok-search-input-border-color: rgba(${borderRgbPartialColor}, 0.3);
+ --ok-search-input-border-radius: 0px;
+ min-width: 120px;
+ font: ${bodyFont};
+ color: ${bodyFontColor};
+ }
+
+ .search-input-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: var(--ok-search-input-height);
+ box-sizing: border-box;
+ border: ${borderWidth} solid transparent;
+ border-radius: var(--ok-search-input-border-radius);
+ color: inherit;
+ background-color: transparent;
+ transition:
+ border-color ${smallDelay} ease-in-out,
+ box-shadow ${smallDelay} ease-in-out,
+ background-color ${smallDelay} ease-in-out;
+ }
+
+ .search-input-container::after {
+ content: '';
+ position: absolute;
+ inset-inline: 0;
+ inset-block-end: calc(-1 * ${borderWidth});
+ border-bottom: calc(${borderWidth} + 1px) solid ${borderHoverColor};
+ transform: scaleX(0);
+ transform-origin: center;
+ transition: transform ${smallDelay} ease-in-out;
+ pointer-events: none;
+ }
+
+ .search-input {
+ -webkit-appearance: none;
+ appearance: none;
+ display: block;
+ flex: 1 1 auto;
+ min-width: 0;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ padding: 0 var(--ok-search-input-trailing-space) 0 var(--ok-search-input-leading-space);
+ font: inherit;
+ line-height: normal;
+ color: inherit;
+ border: none;
+ outline: none;
+ border-radius: 0;
+ background: transparent;
+ }
+
+ .search-input::placeholder {
+ color: ${placeholderFontColor};
+ }
+
+ .search-input-icon,
+ .search-input-clear {
+ position: absolute;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ top: 50%;
+ transform: translateY(-50%);
+ color: ${placeholderFontColor};
+ ${iconColor.cssCustomProperty}: ${placeholderFontColor};
+ }
+
+ .search-input-icon {
+ inset-inline-start: var(--ok-search-input-inline-padding);
+ pointer-events: none;
+ }
+
+ .search-input-clear {
+ -webkit-appearance: none;
+ appearance: none;
+ inset-inline-end: 2px;
+ width: calc(var(--ok-search-input-height) - 6px);
+ height: calc(var(--ok-search-input-height) - 6px);
+ padding: 0;
+ border: none;
+ border-radius: 2px;
+ background: transparent;
+ cursor: pointer;
+ }
+
+ .search-input-clear:hover {
+ background: ${fillHoverColor};
+ }
+
+ .search-input-clear:focus-visible {
+ outline: ${borderWidth} solid ${borderHoverColor};
+ outline-offset: -1px;
+ }
+
+ .search-input:focus-visible {
+ outline: none;
+ }
+
+ :host([appearance='outline']) .search-input-container {
+ border-color: var(--ok-search-input-border-color);
+ }
+
+ :host([appearance='outline']) .search-input-container:hover,
+ :host([appearance='outline']) .search-input-container:focus-within {
+ border-color: ${borderHoverColor};
+ box-shadow: 0 0 0 ${borderWidth} ${borderHoverColor} inset;
+ }
+
+ :host([appearance='block']) .search-input-container {
+ background-color: rgba(${borderRgbPartialColor}, 0.1);
+ }
+
+ :host([appearance='block']) .search-input-container:hover::after,
+ :host([appearance='block']) .search-input-container:focus-within::after,
+ :host([appearance='ghost']) .search-input-container::after,
+ :host([appearance='super-ghost']) .search-input-container:hover::after,
+ :host([appearance='super-ghost']) .search-input-container:focus-within::after {
+ transform: scaleX(1);
+ }
+
+ :host([appearance='ghost']) .search-input-container::after {
+ border-bottom-color: var(--ok-search-input-border-color);
+ }
+
+ :host([appearance='ghost']) .search-input-container:hover::after,
+ :host([appearance='ghost']) .search-input-container:focus-within::after,
+ :host([appearance='super-ghost']) .search-input-container:hover::after,
+ :host([appearance='super-ghost']) .search-input-container:focus-within::after,
+ :host([appearance='block']) .search-input-container:hover::after,
+ :host([appearance='block']) .search-input-container:focus-within::after {
+ border-bottom-color: ${borderHoverColor};
+ }
+`;
\ No newline at end of file
diff --git a/packages/ok-components/src/search-input/template.ts b/packages/ok-components/src/search-input/template.ts
new file mode 100644
index 0000000000..7d0200d408
--- /dev/null
+++ b/packages/ok-components/src/search-input/template.ts
@@ -0,0 +1,34 @@
+import { html, ref, when } from '@ni/fast-element';
+import { iconMagnifyingGlassTag } from '@ni/nimble-components/dist/esm/icons/magnifying-glass';
+import { iconTimesTag } from '@ni/nimble-components/dist/esm/icons/times';
+import type { SearchInput } from '.';
+
+export const template = html`
+
+
+ <${iconMagnifyingGlassTag}>${iconMagnifyingGlassTag}>
+
+ x.handleInput(c.event)}"
+ @change="${(x, c) => x.handleChange(c.event)}"
+ />
+ ${when(
+ x => x.value.length > 0,
+ html`
+
+ `
+ )}
+
+`;
\ No newline at end of file
diff --git a/packages/ok-components/src/search-input/tests/search-input.spec.ts b/packages/ok-components/src/search-input/tests/search-input.spec.ts
new file mode 100644
index 0000000000..d2cd2532ee
--- /dev/null
+++ b/packages/ok-components/src/search-input/tests/search-input.spec.ts
@@ -0,0 +1,95 @@
+import { html } from '@ni/fast-element';
+import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';
+import { fixture, type Fixture } from '../../utilities/tests/fixture';
+import { SearchInput, searchInputTag } from '..';
+import { SearchInputAppearance } from '../types';
+
+async function setup(): Promise> {
+ return await fixture(html`
+ <${searchInputTag} placeholder="Search assets">${searchInputTag}>
+ `);
+}
+
+describe('SearchInput', () => {
+ let element: SearchInput;
+ let connect: () => Promise;
+ let disconnect: (() => Promise) | undefined;
+
+ afterEach(async () => {
+ await disconnect?.();
+ disconnect = undefined;
+ });
+
+ it('can construct an element instance', () => {
+ expect(document.createElement(searchInputTag)).toBeInstanceOf(SearchInput);
+ });
+
+ it('renders the configured placeholder', async () => {
+ ({ element, connect, disconnect } = await setup());
+ await connect();
+ await waitForUpdatesAsync();
+
+ const input = element.shadowRoot?.querySelector('input');
+ expect(input?.getAttribute('placeholder')).toBe('Search assets');
+ });
+
+ it('uses a plain text input with a single custom clear button affordance', async () => {
+ ({ element, connect, disconnect } = await setup());
+ element.value = 'asset';
+ await connect();
+ await waitForUpdatesAsync();
+
+ const input = element.shadowRoot?.querySelector('input');
+ expect(input?.getAttribute('type')).toBe('text');
+ expect(element.shadowRoot?.querySelectorAll('.search-input-clear').length).toBe(1);
+ });
+
+ it('clears the value when the clear button is clicked', async () => {
+ ({ element, connect, disconnect } = await setup());
+ element.value = 'asset';
+ await connect();
+ await waitForUpdatesAsync();
+
+ const clearButton = element.shadowRoot?.querySelector('.search-input-clear');
+ clearButton?.click();
+ await waitForUpdatesAsync();
+
+ expect(element.value).toBe('');
+ });
+
+ it('emits one input and one change event for a single input interaction', async () => {
+ ({ element, connect, disconnect } = await setup());
+ const inputSpy = jasmine.createSpy('input');
+ const changeSpy = jasmine.createSpy('change');
+ await connect();
+ await waitForUpdatesAsync();
+
+ element.addEventListener('input', inputSpy);
+ element.addEventListener('change', changeSpy);
+
+ const input = element.shadowRoot?.querySelector('input');
+ input!.value = 'asset';
+ input!.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+ input!.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
+ await waitForUpdatesAsync();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ expect(changeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('defaults to outline appearance', async () => {
+ ({ element, connect, disconnect } = await setup());
+ await connect();
+
+ expect(element.appearance).toBe(SearchInputAppearance.outline);
+ });
+
+ it('supports super-ghost appearance', async () => {
+ ({ element, connect, disconnect } = await setup());
+ element.appearance = SearchInputAppearance.superGhost;
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(element.appearance).toBe(SearchInputAppearance.superGhost);
+ });
+});
\ No newline at end of file
diff --git a/packages/ok-components/src/search-input/types.ts b/packages/ok-components/src/search-input/types.ts
new file mode 100644
index 0000000000..af9460037b
--- /dev/null
+++ b/packages/ok-components/src/search-input/types.ts
@@ -0,0 +1,8 @@
+export const SearchInputAppearance = {
+ block: 'block',
+ outline: 'outline',
+ ghost: 'ghost',
+ superGhost: 'super-ghost'
+} as const;
+
+export type SearchInputAppearance = (typeof SearchInputAppearance)[keyof typeof SearchInputAppearance];
\ No newline at end of file
diff --git a/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts b/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts
new file mode 100644
index 0000000000..1c4c92481e
--- /dev/null
+++ b/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts
@@ -0,0 +1,97 @@
+import type { Meta, StoryFn } from '@storybook/html-vite';
+import { html, ViewTemplate } from '@ni/fast-element';
+import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers';
+import { searchInputTag } from '@ni/ok-components/dist/esm/search-input';
+import { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types';
+import {
+ createMatrixInteractionsFromStates,
+ createMatrixThemeStory,
+ sharedMatrixParameters
+} from '../../utilities/matrix';
+
+const metadata: Meta = {
+ title: 'Tests Ok/Search Input',
+ parameters: {
+ ...sharedMatrixParameters()
+ }
+};
+
+export default metadata;
+
+type SearchInputMatrixState = readonly [string, keyof typeof SearchInputAppearance];
+
+const searchInputStates: SearchInputMatrixState[] = [
+ ['block', 'block'],
+ ['outline', 'outline'],
+ ['ghost', 'ghost'],
+ ['super-ghost', 'superGhost']
+];
+
+const searchField = (
+ label: string,
+ appearance: keyof typeof SearchInputAppearance
+): ViewTemplate => html`
+
+
${() => label}
+ <${searchInputTag}
+ appearance="${() => SearchInputAppearance[appearance]}"
+ placeholder="Search..."
+ style="width: 124px; --ok-search-input-height: 32px;"
+ >${searchInputTag}>
+
+`;
+
+export const statesThemeMatrix: StoryFn = createMatrixThemeStory(html`
+
+ ${searchField('Search_Block_Light_32', 'block')}
+ ${searchField('Search_Outline_Light_32', 'outline')}
+ ${searchField('Search_Ghost_Light_32', 'ghost')}
+ ${searchField('Search_SuperGhost_Light_32', 'superGhost')}
+
+`);
+
+export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory(
+ createMatrixInteractionsFromStates(
+ (label: string, appearance: keyof typeof SearchInputAppearance): ViewTemplate => html`
+
+
${() => label}
+ <${searchInputTag}
+ appearance="${() => SearchInputAppearance[appearance]}"
+ placeholder="Search..."
+ style="width: 124px; --ok-search-input-height: 32px;"
+ >${searchInputTag}>
+
+ `,
+ {
+ hover: searchInputStates,
+ hoverActive: [],
+ active: [],
+ focus: searchInputStates
+ }
+ )
+);
+
+export const typedThemeMatrix: StoryFn = createMatrixThemeStory(html`
+
+ ${searchField('Typed_Block_Light_32', 'block')}
+ ${searchField('Typed_Outline_Light_32', 'outline')}
+ ${searchField('Typed_Ghost_Light_32', 'ghost')}
+ ${searchField('Typed_SuperGhost_Light_32', 'superGhost')}
+
+`);
+
+typedThemeMatrix.play = async ({ step }): Promise => {
+ const searchInputs = Array.from(document.querySelectorAll(searchInputTag));
+
+ await step('Type search text into each input', async () => {
+ await Promise.all(searchInputs.map(async searchInput => {
+ const input = searchInput.shadowRoot?.querySelector('input');
+
+ if (input) {
+ input.value = 'Search';
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+ await waitForUpdatesAsync();
+ }
+ }));
+ });
+};
\ No newline at end of file
diff --git a/packages/storybook/src/ok/search-input/search-input.mdx b/packages/storybook/src/ok/search-input/search-input.mdx
new file mode 100644
index 0000000000..bf62e0c892
--- /dev/null
+++ b/packages/storybook/src/ok/search-input/search-input.mdx
@@ -0,0 +1,15 @@
+import { Canvas, Controls, Meta, Title } from '@storybook/addon-docs/blocks';
+import * as searchInputStories from './search-input.stories';
+
+
+
+
+A search field with a leading magnifying glass icon and a clear affordance when a value is present.
+
+It supports `block`, `outline`, `ghost`, and `super-ghost` appearances. Height can be adjusted by client CSS via `--ok-search-input-height` rather than separate built-in size variants.
+
+
+
+## API
+
+
\ No newline at end of file
diff --git a/packages/storybook/src/ok/search-input/search-input.stories.ts b/packages/storybook/src/ok/search-input/search-input.stories.ts
new file mode 100644
index 0000000000..3a718c0ed6
--- /dev/null
+++ b/packages/storybook/src/ok/search-input/search-input.stories.ts
@@ -0,0 +1,66 @@
+import type { Meta, StoryObj } from '@storybook/html-vite';
+import { html } from '@ni/fast-element';
+import { searchInputTag } from '@ni/ok-components/dist/esm/search-input';
+import { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types';
+import {
+ appearanceDescription,
+ apiCategory,
+ createUserSelectedThemeStory,
+ placeholderDescription
+} from '../../utilities/storybook';
+
+interface SearchInputArgs {
+ appearance: keyof typeof SearchInputAppearance;
+ placeholder: string;
+ initialSearchValue: string;
+ input?: (e: Event) => void;
+ change?: (e: Event) => void;
+}
+
+const metadata: Meta = {
+ title: 'Ok/Search Input',
+ render: createUserSelectedThemeStory(html`
+
+ <${searchInputTag}
+ appearance="${x => SearchInputAppearance[x.appearance]}"
+ placeholder="${x => x.placeholder}"
+ value="${x => x.initialSearchValue}"
+ >${searchInputTag}>
+
+ `),
+ argTypes: {
+ appearance: {
+ description: appearanceDescription({ componentName: 'search input' }),
+ options: Object.keys(SearchInputAppearance),
+ control: { type: 'radio' },
+ table: { category: apiCategory.attributes }
+ },
+ placeholder: {
+ description: placeholderDescription({ componentName: 'search input' }),
+ table: { category: apiCategory.attributes }
+ },
+ initialSearchValue: {
+ description: 'Initial text rendered in the search input.',
+ table: { category: apiCategory.nonAttributeProperties }
+ },
+ input: {
+ table: { category: apiCategory.events },
+ control: false
+ },
+ change: {
+ table: { category: apiCategory.events },
+ control: false
+ }
+ },
+ args: {
+ appearance: 'outline',
+ placeholder: 'Search',
+ initialSearchValue: ''
+ }
+};
+
+export default metadata;
+
+export const defaultStory: StoryObj = {
+ name: 'default'
+};
\ No newline at end of file