@stoovles/gap-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.js +2417 -0
  2. package/dist/styles.css +4309 -0
  3. package/guidelines/components/accordion.md +99 -0
  4. package/guidelines/components/breadcrumb.md +74 -0
  5. package/guidelines/components/buttons.md +90 -0
  6. package/guidelines/components/checkbox.md +86 -0
  7. package/guidelines/components/chip.md +77 -0
  8. package/guidelines/components/divider.md +52 -0
  9. package/guidelines/components/dropdown.md +125 -0
  10. package/guidelines/components/filter-sort.md +108 -0
  11. package/guidelines/components/fulfillment.md +100 -0
  12. package/guidelines/components/global-footer.md +71 -0
  13. package/guidelines/components/global-header.md +67 -0
  14. package/guidelines/components/hamburger-nav.md +79 -0
  15. package/guidelines/components/handle.md +49 -0
  16. package/guidelines/components/icons.md +147 -0
  17. package/guidelines/components/link.md +70 -0
  18. package/guidelines/components/logo.md +63 -0
  19. package/guidelines/components/mega-nav.md +103 -0
  20. package/guidelines/components/notification.md +97 -0
  21. package/guidelines/components/pagination.md +88 -0
  22. package/guidelines/components/popover.md +77 -0
  23. package/guidelines/components/price.md +86 -0
  24. package/guidelines/components/product-card.md +82 -0
  25. package/guidelines/components/radio.md +92 -0
  26. package/guidelines/components/search-input.md +63 -0
  27. package/guidelines/components/selector-swatch.md +78 -0
  28. package/guidelines/components/selector.md +95 -0
  29. package/guidelines/components/slider.md +116 -0
  30. package/guidelines/components/switch.md +108 -0
  31. package/guidelines/components/tabs.md +83 -0
  32. package/guidelines/components/text-input.md +80 -0
  33. package/guidelines/composition/buy-box.md +132 -0
  34. package/guidelines/composition/recommendations.md +100 -0
  35. package/package.json +38 -0
@@ -0,0 +1,99 @@
1
+ # Accordion
2
+
3
+ ## When to use
4
+
5
+ Use the `Accordion` to present a vertically stacked list of sections that can be individually expanded or collapsed. Common uses include FAQs, filter panels, product detail sections, and settings groups.
6
+
7
+ ```tsx
8
+ import { Accordion, AccordionItem } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Props
12
+
13
+ ### Accordion
14
+
15
+ | Prop | Type | Default | Description |
16
+ |---|---|---|---|
17
+ | `children` | `ReactNode` | — | One or more `AccordionItem` elements |
18
+ | `className` | `string` | — | Additional CSS class |
19
+
20
+ ### AccordionItem
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `title` | `string` | — | Header text for the section |
25
+ | `count` | `string \| number` | — | Optional count displayed after the title, e.g. `(3)` |
26
+ | `subtitle` | `string` | — | Secondary text shown below the title when collapsed |
27
+ | `attributes` | `string` | — | Summary text shown below the title when expanded (e.g. selected filter values) |
28
+ | `defaultExpanded` | `boolean` | `false` | Whether the item starts expanded |
29
+ | `children` | `ReactNode` | — | Content revealed when expanded |
30
+ | `className` | `string` | — | Additional CSS class |
31
+
32
+ ## Visual reference
33
+
34
+ | State | Chevron | Subtitle | Content |
35
+ |---|---|---|---|
36
+ | Collapsed | Points down | Visible (if provided) | Hidden |
37
+ | Expanded | Points up | Hidden | Visible |
38
+
39
+ Each item is separated by a subtle divider. The chevron rotates 180 degrees with a smooth transition on expand/collapse.
40
+
41
+ ## Sizing
42
+
43
+ - Item bottom padding: 24px (`--spacing-l`)
44
+ - Header-to-content gap: 24px (`--spacing-l`)
45
+ - Title-to-subtitle gap: 8px (`--spacing-s`)
46
+ - Title font: Gap Sans, 16px (`--text-header-font-size`), weight 700 (`--font-weight-base-heavier`), letter-spacing 0.28px
47
+ - Count font: same as title
48
+ - Subtitle font: Gap Sans, 14px (`--text-subhead-font-size`), weight 400, secondary color (`--color-type-secondary`)
49
+ - Chevron: 10 × 6px, stroked, `currentColor`
50
+
51
+ ## Examples
52
+
53
+ ```tsx
54
+ {/* Basic accordion with static content */}
55
+ <Accordion>
56
+ <AccordionItem title="Shipping & Returns">
57
+ <p>Free shipping on orders over $50. Returns accepted within 30 days.</p>
58
+ </AccordionItem>
59
+ <AccordionItem title="Size Guide">
60
+ <p>Refer to our size chart for the perfect fit.</p>
61
+ </AccordionItem>
62
+ <AccordionItem title="Product Details">
63
+ <p>100% organic cotton. Machine wash cold.</p>
64
+ </AccordionItem>
65
+ </Accordion>
66
+
67
+ {/* With counts and subtitles (e.g. filter panel) */}
68
+ <Accordion>
69
+ <AccordionItem title="Color" count={3} subtitle="Blue, Red, Green">
70
+ {/* Checkbox filters here */}
71
+ </AccordionItem>
72
+ <AccordionItem title="Size" count={2} subtitle="M, L">
73
+ {/* Checkbox filters here */}
74
+ </AccordionItem>
75
+ </Accordion>
76
+
77
+ {/* With expanded attributes summary */}
78
+ <Accordion>
79
+ <AccordionItem
80
+ title="Color"
81
+ count={3}
82
+ subtitle="3 selected"
83
+ attributes="Blue, Red, Green"
84
+ defaultExpanded
85
+ >
86
+ {/* Filter chips or checkboxes */}
87
+ </AccordionItem>
88
+ </Accordion>
89
+ ```
90
+
91
+ ## Rules
92
+
93
+ - Always provide a `title` for each `AccordionItem`
94
+ - Use `subtitle` for a brief collapsed-state summary and `attributes` for an expanded-state summary — only one is visible at a time
95
+ - Place any content type inside the expanded area: text, form controls, images, other components
96
+ - Use `count` to communicate the number of items or selections within a section
97
+ - Each item manages its own open/closed state independently — multiple items can be open at once
98
+ - The subtle divider at the top of each item comes from the existing `Divider` component styles
99
+ - Do not nest accordions inside accordions
@@ -0,0 +1,74 @@
1
+ # Breadcrumb
2
+
3
+ ## When to use
4
+
5
+ Use the `Breadcrumb` component to show the user's current location within a site hierarchy and provide quick navigation back to parent pages. Common on product detail pages, category pages, and multi-step flows.
6
+
7
+ ```tsx
8
+ import { Breadcrumb } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Props
12
+
13
+ ### Breadcrumb
14
+
15
+ | Prop | Type | Default | Description |
16
+ |---|---|---|---|
17
+ | `items` | `BreadcrumbItem[]` | — | Ordered list of links from root to current page |
18
+ | `className` | `string` | — | Additional CSS class |
19
+
20
+ ### BreadcrumbItem
21
+
22
+ | Field | Type | Description |
23
+ |---|---|---|
24
+ | `label` | `string` | Display text for the crumb |
25
+ | `href` | `string` | URL the crumb links to |
26
+
27
+ ## Visual reference
28
+
29
+ | Element | Style |
30
+ |---|---|
31
+ | Link text | Gap Sans, 16px, weight 400, link color (`--color-type-link`), underlined |
32
+ | Last link (multi-level) | Same color, no underline, `aria-current="page"` |
33
+ | Separator `/` | Gap Sans, 16px, weight 400, primary color (`--color-type-primary`) |
34
+
35
+ ## Sizing
36
+
37
+ - Vertical padding: 16px (`--spacing-utk-l`)
38
+ - Gap between crumbs and separators: 4px (`--spacing-xs`)
39
+ - Font size: 16px (`--text-link-regular-font-size`)
40
+ - Letter spacing: 0.32px (`--font-letter-spacing-0`)
41
+
42
+ ## Examples
43
+
44
+ ```tsx
45
+ {/* Single-level breadcrumb */}
46
+ <Breadcrumb items={[{ label: "Women", href: "/women" }]} />
47
+
48
+ {/* Two-level breadcrumb */}
49
+ <Breadcrumb
50
+ items={[
51
+ { label: "Women", href: "/women" },
52
+ { label: "Dresses", href: "/women/dresses" },
53
+ ]}
54
+ />
55
+
56
+ {/* Deep navigation */}
57
+ <Breadcrumb
58
+ items={[
59
+ { label: "Home", href: "/" },
60
+ { label: "Women", href: "/women" },
61
+ { label: "Clothing", href: "/women/clothing" },
62
+ { label: "Dresses", href: "/women/clothing/dresses" },
63
+ ]}
64
+ />
65
+ ```
66
+
67
+ ## Rules
68
+
69
+ - Always provide at least one item — the component renders nothing for an empty array
70
+ - Order items from the broadest category (root) to the most specific (current page)
71
+ - The last item in a multi-level breadcrumb receives `aria-current="page"` and no underline
72
+ - The component renders inside a `<nav aria-label="Breadcrumb">` with a semantic `<ol>` for accessibility
73
+ - Use concise labels — breadcrumb text should match the target page's title or heading
74
+ - Do not use breadcrumbs as the primary navigation; they supplement the main nav
@@ -0,0 +1,90 @@
1
+ # Button
2
+
3
+ ## When to use
4
+
5
+ Use the `Button` component for all interactive actions — form submissions, confirmations, navigation triggers, and destructive operations. Never use a `<div>` or `<a>` when a semantic `<button>` is needed.
6
+
7
+ ```tsx
8
+ import { Button } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `variant` | `"critical" \| "primary" \| "secondary"` | `"primary"` | Visual style of the button |
16
+ | `loading` | `boolean` | `false` | Shows a dot loader and disables interaction |
17
+ | `fullWidth` | `boolean` | `false` | Stretches button to fill its container |
18
+ | `disabled` | `boolean` | `false` | Grays out the button and prevents clicks |
19
+ | `children` | `ReactNode` | — | Button label text |
20
+ | `className` | `string` | — | Additional CSS class |
21
+
22
+ All standard `<button>` HTML attributes are also supported (e.g. `type`, `onClick`, `aria-label`).
23
+
24
+ ## Variant decision tree
25
+
26
+ ```
27
+ What action does this button perform?
28
+
29
+ ├─ A destructive or high-stakes action (delete, cancel order, remove)?
30
+ │ └─ variant="critical"
31
+
32
+ ├─ The primary call-to-action on the page (add to bag, checkout, submit)?
33
+ │ └─ variant="primary"
34
+
35
+ └─ A secondary or supporting action (save for later, continue shopping)?
36
+ └─ variant="secondary"
37
+ ```
38
+
39
+ ## Visual reference
40
+
41
+ | Variant | Default | Hover | Active | Disabled |
42
+ |---|---|---|---|---|
43
+ | `critical` | Blue fill (#031ba1), white text | Black fill, white text | Blue fill, white text | Gray fill (#ededed), gray text |
44
+ | `primary` | White fill, gray border (#ccc), black text | Blue fill (#031ba1), black border, white text | White fill, black border, black text | White fill, gray border (#ededed), gray text |
45
+ | `secondary` | Black fill, black border, white text | Blue fill (#031ba1), blue border, white text | Blue fill, blue border, white text | Gray fill (#ededed), gray text |
46
+
47
+ ## Sizing
48
+
49
+ - Minimum height: 44px (touch target compliance)
50
+ - Horizontal padding: 48px
51
+ - Border radius: 0px (Gap's square-corner identity)
52
+ - Font: Gap Sans, 16px, weight 400, letter-spacing 0.32px
53
+
54
+ ## Examples
55
+
56
+ ```tsx
57
+ {/* Primary CTA */}
58
+ <Button variant="primary">Add to Bag</Button>
59
+
60
+ {/* Secondary action */}
61
+ <Button variant="secondary">Save for Later</Button>
62
+
63
+ {/* Destructive action */}
64
+ <Button variant="critical">Remove Item</Button>
65
+
66
+ {/* Loading state (e.g. during form submission) */}
67
+ <Button variant="secondary" loading>
68
+ Processing
69
+ </Button>
70
+
71
+ {/* Disabled */}
72
+ <Button variant="primary" disabled>
73
+ Select a Size
74
+ </Button>
75
+
76
+ {/* Full-width in a narrow container */}
77
+ <Button variant="secondary" fullWidth>
78
+ Checkout
79
+ </Button>
80
+ ```
81
+
82
+ ## Rules
83
+
84
+ - Every page should have at most one `critical` button visible at a time
85
+ - Prefer `primary` for the main CTA; use `secondary` for supporting actions
86
+ - Never place two `critical` buttons side by side
87
+ - Always provide a text label — icon-only actions should use a different component
88
+ - Use `loading` during async operations instead of manually disabling and re-enabling
89
+ - Do not override the 44px minimum height — it ensures touch accessibility
90
+ - When pairing buttons, place `primary` or `critical` on the right, `secondary` on the left
@@ -0,0 +1,86 @@
1
+ # Checkbox
2
+
3
+ ## When to use
4
+
5
+ Use `Checkbox` when the user needs to select zero or more options from a list, or toggle a single boolean setting (e.g. "Remember me"). For mutually-exclusive choices use a radio group instead.
6
+
7
+ ```tsx
8
+ import { Checkbox, CheckboxGroup } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Checkbox Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `label` | `ReactNode` | — | Text label displayed next to the checkbox |
16
+ | `error` | `boolean` | `false` | Renders the checkbox in the error state (red border / fill) |
17
+ | `disabled` | `boolean` | `false` | Grays out the checkbox and prevents interaction |
18
+ | `checked` | `boolean` | — | Controlled checked state |
19
+ | `defaultChecked` | `boolean` | — | Uncontrolled initial checked state |
20
+ | `onChange` | `(e) => void` | — | Change handler |
21
+ | `className` | `string` | — | Additional CSS class |
22
+
23
+ All standard `<input>` HTML attributes are also supported (e.g. `name`, `value`, `required`).
24
+
25
+ ## CheckboxGroup Props
26
+
27
+ | Prop | Type | Default | Description |
28
+ |---|---|---|---|
29
+ | `children` | `ReactNode` | — | `Checkbox` components |
30
+ | `label` | `string` | — | Group legend (optional) |
31
+ | `error` | `boolean` | `false` | Applies error styling to the group wrapper |
32
+ | `className` | `string` | — | Additional CSS class |
33
+
34
+ ## Visual reference
35
+
36
+ | State | Unchecked | Checked |
37
+ |---|---|---|
38
+ | Default | White fill, black 1px border | Dark gray fill (#595959), black border, white checkmark |
39
+ | Error | White fill, red border | Red fill (#d00000), red border, white checkmark |
40
+ | Disabled | White fill, gray border (#ededed) | Gray fill (#ededed), gray border, white checkmark |
41
+
42
+ ## Sizing
43
+
44
+ - Checkbox box: 22×22px inside a 24×24px touch target
45
+ - Border radius: 0px (Gap square-corner identity)
46
+ - Gap between box and label: 8px
47
+ - Group spacing between items: 24px
48
+ - Font: Gap Sans, 16px, weight 400
49
+
50
+ ## Examples
51
+
52
+ ```tsx
53
+ {/* Single checkbox */}
54
+ <Checkbox label="I agree to the terms" />
55
+
56
+ {/* Controlled checkbox */}
57
+ <Checkbox
58
+ label="Subscribe to newsletter"
59
+ checked={subscribed}
60
+ onChange={(e) => setSubscribed(e.target.checked)}
61
+ />
62
+
63
+ {/* Checkbox group */}
64
+ <CheckboxGroup label="Select sizes">
65
+ <Checkbox label="XS" name="size" value="xs" />
66
+ <Checkbox label="S" name="size" value="s" />
67
+ <Checkbox label="M" name="size" value="m" />
68
+ <Checkbox label="L" name="size" value="l" />
69
+ <Checkbox label="XL" name="size" value="xl" />
70
+ </CheckboxGroup>
71
+
72
+ {/* Error state */}
73
+ <Checkbox label="Required field" error />
74
+
75
+ {/* Disabled */}
76
+ <Checkbox label="Out of stock" disabled checked />
77
+ ```
78
+
79
+ ## Rules
80
+
81
+ - Always provide a `label` — a checkbox without visible text needs an `aria-label`
82
+ - Use `CheckboxGroup` with a `label` (rendered as `<legend>`) when presenting related options
83
+ - Never use a checkbox for a single mutually-exclusive choice — use a radio button instead
84
+ - Place the checkbox to the left of its label
85
+ - In error states, validate on form submission or blur, not on every keystroke
86
+ - Do not mix error and disabled states on the same checkbox
@@ -0,0 +1,77 @@
1
+ # Chip
2
+
3
+ ## When to use
4
+
5
+ Use `Chip` to display an active filter or attribute that the user can dismiss. Chips are typically shown in a `ChipGroup` after filter selections on product listing and search results pages.
6
+
7
+ ```tsx
8
+ import { Chip, ChipGroup } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Chip Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `label` | `string` | — | The filter attribute text (e.g. "Blue", "Size M") |
16
+ | `onDismiss` | `() => void` | — | Callback when the dismiss (×) button is clicked. Omit to hide the dismiss button. |
17
+ | `swatch` | `string` | — | CSS color value for an optional color swatch circle (e.g. `"#031ba1"`) |
18
+ | `className` | `string` | — | Additional CSS class |
19
+
20
+ ## ChipGroup Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `children` | `ReactNode` | — | `Chip` components |
25
+ | `onClearAll` | `() => void` | — | Callback for the "Clear all" link. Omit to hide the link. |
26
+ | `clearAllLabel` | `string` | `"Clear all"` | Label for the clear-all action |
27
+ | `className` | `string` | — | Additional CSS class |
28
+
29
+ ## Visual reference
30
+
31
+ | State | Appearance |
32
+ |---|---|
33
+ | Default | Semi-transparent gray background, blue text (#031ba1), no visible border |
34
+ | Hover | 1px border appears |
35
+ | Focus | Gray background (#ccc), 1px border |
36
+ | Active | Darker gray background (#afafaf) |
37
+
38
+ ## Sizing
39
+
40
+ - Min height: 32px, min width: 48px
41
+ - Padding: 8px vertical, 12px horizontal
42
+ - Gap between chip elements: 8px
43
+ - Border radius: 0px (Gap square-corner identity)
44
+ - Font: Gap Sans, 16px, letter-spacing 0.24px
45
+ - Dismiss icon: 9×9px × in a 16px touch target
46
+ - Swatch: 16px circle (fully round)
47
+ - Group chip gap: 12px row, 8px column
48
+
49
+ ## Examples
50
+
51
+ ```tsx
52
+ {/* Single dismissible chip */}
53
+ <Chip label="Blue" onDismiss={() => removeFilter('blue')} />
54
+
55
+ {/* Chip with color swatch */}
56
+ <Chip label="Navy" swatch="#031ba1" onDismiss={() => removeFilter('navy')} />
57
+
58
+ {/* Chip without dismiss (read-only display) */}
59
+ <Chip label="On Sale" />
60
+
61
+ {/* ChipGroup with clear-all */}
62
+ <ChipGroup onClearAll={() => clearAllFilters()}>
63
+ <Chip label="Size M" onDismiss={() => removeFilter('size-m')} />
64
+ <Chip label="Blue" swatch="#031ba1" onDismiss={() => removeFilter('blue')} />
65
+ <Chip label="Under $50" onDismiss={() => removeFilter('under-50')} />
66
+ </ChipGroup>
67
+ ```
68
+
69
+ ## Rules
70
+
71
+ - Always include a meaningful `label` — do not use chips with only a swatch
72
+ - Provide `onDismiss` for every chip when the user should be able to remove individual filters
73
+ - Use `ChipGroup` with `onClearAll` when displaying 2 or more active filters
74
+ - Place the `ChipGroup` above the product grid, below the filter bar
75
+ - Use the `swatch` prop only for color-based filters
76
+ - Keep labels concise — one or two words maximum
77
+ - Do not use chips for navigation or as toggle buttons; use them only for applied filters
@@ -0,0 +1,52 @@
1
+ # Divider
2
+
3
+ ## When to use
4
+
5
+ Use the `Divider` component to visually separate sections of content. It renders a semantic `<hr>` element.
6
+
7
+ ```tsx
8
+ import { Divider } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `variant` | `"default" \| "subtle"` | `"default"` | Default is dark (#595959), subtle is light (#ccc) |
16
+ | `className` | `string` | — | Additional CSS class |
17
+
18
+ ## Variant decision tree
19
+
20
+ ```
21
+ How prominent should the separation be?
22
+
23
+ ├─ Clear visual break between major sections?
24
+ │ └─ variant="default" (dark line)
25
+
26
+ └─ Gentle separation between related items (e.g. list rows)?
27
+ └─ variant="subtle" (light line)
28
+ ```
29
+
30
+ ## Sizing
31
+
32
+ - Width: 100% of parent container
33
+ - Border width: 2px (`--divider-border-width`)
34
+ - Default color: #595959 (`--divider-color`)
35
+ - Subtle color: #ccc (`--color-border-subtle`)
36
+
37
+ ## Examples
38
+
39
+ ```tsx
40
+ {/* Default dark divider */}
41
+ <Divider />
42
+
43
+ {/* Subtle divider between list items */}
44
+ <Divider variant="subtle" />
45
+ ```
46
+
47
+ ## Rules
48
+
49
+ - Use `default` for major section breaks (e.g. between content blocks)
50
+ - Use `subtle` for lighter separations (e.g. between list items, accordion rows)
51
+ - Do not stack multiple dividers — one is sufficient
52
+ - Spacing above and below the divider should be controlled by the parent layout, not the divider itself
@@ -0,0 +1,125 @@
1
+ # Dropdown
2
+
3
+ ## When to use
4
+
5
+ Use the `Dropdown` component when the user needs to select a single value from a predefined list of options. Appropriate for form fields like country, size, sort order, or any finite set of choices.
6
+
7
+ ```tsx
8
+ import { Dropdown } from '@stoovles/gap-kit'
9
+ ```
10
+
11
+ ## Props
12
+
13
+ ### Dropdown
14
+
15
+ | Prop | Type | Default | Description |
16
+ |---|---|---|---|
17
+ | `label` | `string` | — | Field label (shown as placeholder when empty, floats when filled) |
18
+ | `options` | `DropdownOption[]` | — | List of selectable options |
19
+ | `value` | `string` | — | Controlled selected value |
20
+ | `defaultValue` | `string` | — | Uncontrolled initial value |
21
+ | `onChange` | `(value: string) => void` | — | Callback when selection changes |
22
+ | `error` | `boolean` | `false` | Renders the field in error state with red border |
23
+ | `disabled` | `boolean` | `false` | Grays out the field and prevents interaction |
24
+ | `className` | `string` | — | Additional CSS class |
25
+ | `id` | `string` | — | HTML id for the trigger button |
26
+
27
+ ### DropdownOption
28
+
29
+ | Field | Type | Description |
30
+ |---|---|---|
31
+ | `label` | `string` | Display text shown in the menu and trigger |
32
+ | `value` | `string` | Unique value passed to `onChange` |
33
+
34
+ ## Visual reference
35
+
36
+ | State | Border | Label | Background |
37
+ |---|---|---|---|
38
+ | Empty (closed) | 1px gray (`--dropdown-border-color`) | Inline as placeholder (subtle color) | White |
39
+ | Open | 2px gray (`--dropdown-border-color`) | Inline or floating | White |
40
+ | Filled (closed) | 1px gray | Floating above field | White |
41
+ | Error | 1px or 2px red (`--color-border-error`) | Floating, red text | White |
42
+ | Disabled | No visible border | Floating, subtle color | Gray (`--color-fill-disabled`) |
43
+
44
+ ### Menu rows
45
+
46
+ | State | Background | Text color |
47
+ |---|---|---|
48
+ | Default | White | Primary (`--color-type-primary`) |
49
+ | Hover / Active | Light gray (`--color-gray-2`) | Primary |
50
+ | Selected | Dark blue (`--dropdown-fill-selected`) | White (`--dropdown-font-color`) |
51
+
52
+ ## Sizing
53
+
54
+ - Trigger height: 44px
55
+ - Horizontal padding: 16px (`--spacing-utk-l`)
56
+ - Border radius: 0px (Gap square-corner identity)
57
+ - Font: Gap Sans, 16px, weight 400, 0.32px letter-spacing
58
+ - Floating label: 10px, positioned above the border
59
+ - Menu row height: 42px, padding 12px vertical / 16px horizontal
60
+ - Menu max-height: 252px (scrollable)
61
+ - Menu shadow: 2px 4px 8px rgba(0,0,0,0.08)
62
+
63
+ ## Examples
64
+
65
+ ```tsx
66
+ {/* Basic dropdown */}
67
+ <Dropdown
68
+ label="Size"
69
+ options={[
70
+ { label: "Small", value: "s" },
71
+ { label: "Medium", value: "m" },
72
+ { label: "Large", value: "l" },
73
+ { label: "X-Large", value: "xl" },
74
+ ]}
75
+ />
76
+
77
+ {/* Controlled dropdown */}
78
+ const [sort, setSort] = useState("");
79
+ <Dropdown
80
+ label="Sort By"
81
+ value={sort}
82
+ onChange={setSort}
83
+ options={[
84
+ { label: "Newest", value: "newest" },
85
+ { label: "Price: Low to High", value: "price-asc" },
86
+ { label: "Price: High to Low", value: "price-desc" },
87
+ ]}
88
+ />
89
+
90
+ {/* Error state */}
91
+ <Dropdown
92
+ label="Country"
93
+ options={countries}
94
+ error={!selectedCountry}
95
+ />
96
+
97
+ {/* Disabled */}
98
+ <Dropdown
99
+ label="Region"
100
+ defaultValue="us"
101
+ options={[{ label: "United States", value: "us" }]}
102
+ disabled
103
+ />
104
+ ```
105
+
106
+ ## Keyboard navigation
107
+
108
+ | Key | Action |
109
+ |---|---|
110
+ | `Enter` / `Space` | Open menu or select focused option |
111
+ | `ArrowDown` | Open menu or move focus down |
112
+ | `ArrowUp` | Move focus up |
113
+ | `Escape` | Close menu |
114
+ | `Tab` | Close menu and move focus |
115
+
116
+ ## Rules
117
+
118
+ - Always provide a `label` — it acts as both placeholder and floating label
119
+ - Each option must have a unique `value`
120
+ - The menu appears directly below the trigger and matches its width
121
+ - The selected option is highlighted with the brand blue background
122
+ - Use `error` for validation — the border and floating label turn red
123
+ - Do not use a dropdown for fewer than 3 options — use `Radio` instead
124
+ - Do not use a dropdown for more than ~15 options — consider a searchable input
125
+ - The component supports both controlled (`value` + `onChange`) and uncontrolled (`defaultValue`) patterns