remix 3.0.0-rc.4 → 3.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 (191) hide show
  1. package/INDEX.md +6 -21
  2. package/README.md +1 -1
  3. package/dist/component/dev/refresh.d.ts +2 -0
  4. package/dist/component/dev/refresh.d.ts.map +1 -0
  5. package/dist/{ui → component}/dev/refresh.js +1 -1
  6. package/dist/component/jsx-dev-runtime.d.ts +2 -0
  7. package/dist/component/jsx-dev-runtime.d.ts.map +1 -0
  8. package/dist/{ui → component}/jsx-dev-runtime.js +1 -1
  9. package/dist/component/jsx-runtime.d.ts +2 -0
  10. package/dist/component/jsx-runtime.d.ts.map +1 -0
  11. package/dist/{ui → component}/jsx-runtime.js +1 -1
  12. package/dist/component/server.d.ts +2 -0
  13. package/dist/component/server.d.ts.map +1 -0
  14. package/dist/{ui → component}/server.js +1 -1
  15. package/dist/component/test.d.ts +2 -0
  16. package/dist/component/test.d.ts.map +1 -0
  17. package/dist/{ui → component}/test.js +1 -1
  18. package/dist/component-hmr/assets.d.ts +2 -0
  19. package/dist/component-hmr/assets.d.ts.map +1 -0
  20. package/dist/{ui-hmr.js → component-hmr/assets.js} +1 -1
  21. package/dist/component-hmr/node.d.ts +3 -0
  22. package/dist/component-hmr/node.d.ts.map +1 -0
  23. package/dist/{ui-hmr → component-hmr}/node.js +1 -1
  24. package/dist/component-hmr/runtime/browser.d.ts +2 -0
  25. package/dist/component-hmr/runtime/browser.d.ts.map +1 -0
  26. package/dist/{ui-hmr → component-hmr}/runtime/browser.js +1 -1
  27. package/dist/component-hmr/runtime/server.d.ts +2 -0
  28. package/dist/component-hmr/runtime/server.d.ts.map +1 -0
  29. package/dist/{ui-hmr → component-hmr}/runtime/server.js +1 -1
  30. package/dist/component-hmr.d.ts +2 -0
  31. package/dist/component-hmr.d.ts.map +1 -0
  32. package/dist/{ui-hmr/assets.js → component-hmr.js} +1 -1
  33. package/dist/component.d.ts +2 -0
  34. package/dist/component.d.ts.map +1 -0
  35. package/dist/{ui/anchor.js → component.js} +1 -1
  36. package/guides/01-start-here.md +20 -20
  37. package/guides/02-routing-and-controllers.md +1 -1
  38. package/guides/03-request-handling.md +1 -1
  39. package/guides/04-rendering-ui.md +41 -307
  40. package/guides/05-interactivity.md +42 -37
  41. package/guides/06-streaming-ui-with-frames.md +28 -70
  42. package/guides/07-animation.md +41 -35
  43. package/guides/08-data-and-validation.md +423 -27
  44. package/guides/09-forms-and-mutations.md +250 -25
  45. package/guides/13-testing.md +10 -10
  46. package/guides/14-cli-and-tooling.md +2 -2
  47. package/package.json +96 -180
  48. package/src/assets/README.md +12 -12
  49. package/src/cli/README.md +3 -3
  50. package/src/{ui → component}/README.md +31 -75
  51. package/src/{ui → component}/dev/refresh.ts +1 -1
  52. package/src/{ui → component}/jsx-dev-runtime.ts +1 -1
  53. package/src/{ui → component}/jsx-runtime.ts +1 -1
  54. package/src/{ui → component}/server/README.md +8 -8
  55. package/src/{ui → component}/server.ts +1 -1
  56. package/src/{ui → component}/test/README.md +6 -6
  57. package/src/{ui → component}/test.ts +1 -1
  58. package/src/{ui-hmr → component-hmr}/README.md +13 -13
  59. package/src/{ui-hmr → component-hmr}/assets.ts +1 -1
  60. package/src/{ui-hmr → component-hmr}/node.ts +1 -1
  61. package/src/{ui-hmr → component-hmr}/runtime/browser.ts +1 -1
  62. package/src/{ui-hmr → component-hmr}/runtime/server.ts +1 -1
  63. package/{dist/ui/accordion.js → src/component-hmr.ts} +1 -1
  64. package/{dist/ui/tabs.js → src/component.ts} +1 -1
  65. package/src/data-table/README.md +2 -1
  66. package/src/multiple-import-maps-polyfill/README.md +3 -3
  67. package/src/node-hmr/README.md +6 -6
  68. package/src/render-middleware/README.md +5 -5
  69. package/src/spa/README.md +4 -4
  70. package/src/test/README.md +4 -4
  71. package/dist/ui/accordion/primitives.d.ts +0 -2
  72. package/dist/ui/accordion/primitives.d.ts.map +0 -1
  73. package/dist/ui/accordion/primitives.js +0 -2
  74. package/dist/ui/accordion.d.ts +0 -2
  75. package/dist/ui/accordion.d.ts.map +0 -1
  76. package/dist/ui/anchor.d.ts +0 -2
  77. package/dist/ui/anchor.d.ts.map +0 -1
  78. package/dist/ui/animation.d.ts +0 -2
  79. package/dist/ui/animation.d.ts.map +0 -1
  80. package/dist/ui/animation.js +0 -2
  81. package/dist/ui/breadcrumbs.d.ts +0 -2
  82. package/dist/ui/breadcrumbs.d.ts.map +0 -1
  83. package/dist/ui/breadcrumbs.js +0 -2
  84. package/dist/ui/button.d.ts +0 -3
  85. package/dist/ui/button.d.ts.map +0 -1
  86. package/dist/ui/button.js +0 -3
  87. package/dist/ui/checkbox.d.ts +0 -3
  88. package/dist/ui/checkbox.d.ts.map +0 -1
  89. package/dist/ui/checkbox.js +0 -3
  90. package/dist/ui/combobox/primitives.d.ts +0 -2
  91. package/dist/ui/combobox/primitives.d.ts.map +0 -1
  92. package/dist/ui/combobox/primitives.js +0 -2
  93. package/dist/ui/combobox.d.ts +0 -2
  94. package/dist/ui/combobox.d.ts.map +0 -1
  95. package/dist/ui/combobox.js +0 -2
  96. package/dist/ui/dev/refresh.d.ts +0 -2
  97. package/dist/ui/dev/refresh.d.ts.map +0 -1
  98. package/dist/ui/input.d.ts +0 -3
  99. package/dist/ui/input.d.ts.map +0 -1
  100. package/dist/ui/input.js +0 -3
  101. package/dist/ui/jsx-dev-runtime.d.ts +0 -2
  102. package/dist/ui/jsx-dev-runtime.d.ts.map +0 -1
  103. package/dist/ui/jsx-runtime.d.ts +0 -2
  104. package/dist/ui/jsx-runtime.d.ts.map +0 -1
  105. package/dist/ui/listbox.d.ts +0 -2
  106. package/dist/ui/listbox.d.ts.map +0 -1
  107. package/dist/ui/listbox.js +0 -2
  108. package/dist/ui/menu/primitives.d.ts +0 -2
  109. package/dist/ui/menu/primitives.d.ts.map +0 -1
  110. package/dist/ui/menu/primitives.js +0 -2
  111. package/dist/ui/menu.d.ts +0 -2
  112. package/dist/ui/menu.d.ts.map +0 -1
  113. package/dist/ui/menu.js +0 -2
  114. package/dist/ui/popover.d.ts +0 -2
  115. package/dist/ui/popover.d.ts.map +0 -1
  116. package/dist/ui/popover.js +0 -2
  117. package/dist/ui/radio.d.ts +0 -3
  118. package/dist/ui/radio.d.ts.map +0 -1
  119. package/dist/ui/radio.js +0 -3
  120. package/dist/ui/select/primitives.d.ts +0 -2
  121. package/dist/ui/select/primitives.d.ts.map +0 -1
  122. package/dist/ui/select/primitives.js +0 -2
  123. package/dist/ui/select.d.ts +0 -2
  124. package/dist/ui/select.d.ts.map +0 -1
  125. package/dist/ui/select.js +0 -2
  126. package/dist/ui/server.d.ts +0 -2
  127. package/dist/ui/server.d.ts.map +0 -1
  128. package/dist/ui/tabs/primitives.d.ts +0 -2
  129. package/dist/ui/tabs/primitives.d.ts.map +0 -1
  130. package/dist/ui/tabs/primitives.js +0 -2
  131. package/dist/ui/tabs.d.ts +0 -2
  132. package/dist/ui/tabs.d.ts.map +0 -1
  133. package/dist/ui/test.d.ts +0 -2
  134. package/dist/ui/test.d.ts.map +0 -1
  135. package/dist/ui/toggle/primitives.d.ts +0 -2
  136. package/dist/ui/toggle/primitives.d.ts.map +0 -1
  137. package/dist/ui/toggle/primitives.js +0 -2
  138. package/dist/ui/toggle.d.ts +0 -3
  139. package/dist/ui/toggle.d.ts.map +0 -1
  140. package/dist/ui/toggle.js +0 -3
  141. package/dist/ui-hmr/assets.d.ts +0 -2
  142. package/dist/ui-hmr/assets.d.ts.map +0 -1
  143. package/dist/ui-hmr/node.d.ts +0 -3
  144. package/dist/ui-hmr/node.d.ts.map +0 -1
  145. package/dist/ui-hmr/runtime/browser.d.ts +0 -2
  146. package/dist/ui-hmr/runtime/browser.d.ts.map +0 -1
  147. package/dist/ui-hmr/runtime/server.d.ts +0 -2
  148. package/dist/ui-hmr/runtime/server.d.ts.map +0 -1
  149. package/dist/ui-hmr.d.ts +0 -2
  150. package/dist/ui-hmr.d.ts.map +0 -1
  151. package/dist/ui.d.ts +0 -2
  152. package/dist/ui.d.ts.map +0 -1
  153. package/dist/ui.js +0 -2
  154. package/src/ui/accordion/README.md +0 -202
  155. package/src/ui/accordion/primitives.ts +0 -2
  156. package/src/ui/accordion.ts +0 -2
  157. package/src/ui/anchor/README.md +0 -196
  158. package/src/ui/anchor.ts +0 -2
  159. package/src/ui/animation/README.md +0 -318
  160. package/src/ui/animation.ts +0 -2
  161. package/src/ui/breadcrumbs/README.md +0 -55
  162. package/src/ui/breadcrumbs.ts +0 -2
  163. package/src/ui/button/README.md +0 -44
  164. package/src/ui/button.ts +0 -3
  165. package/src/ui/checkbox/README.md +0 -59
  166. package/src/ui/checkbox.ts +0 -3
  167. package/src/ui/combobox/README.md +0 -194
  168. package/src/ui/combobox/primitives.ts +0 -2
  169. package/src/ui/combobox.ts +0 -2
  170. package/src/ui/input/README.md +0 -52
  171. package/src/ui/input.ts +0 -3
  172. package/src/ui/listbox/README.md +0 -83
  173. package/src/ui/listbox.ts +0 -2
  174. package/src/ui/menu/README.md +0 -161
  175. package/src/ui/menu/primitives.ts +0 -2
  176. package/src/ui/menu.ts +0 -2
  177. package/src/ui/popover/README.md +0 -103
  178. package/src/ui/popover.ts +0 -2
  179. package/src/ui/radio/README.md +0 -53
  180. package/src/ui/radio.ts +0 -3
  181. package/src/ui/select/README.md +0 -117
  182. package/src/ui/select/primitives.ts +0 -2
  183. package/src/ui/select.ts +0 -2
  184. package/src/ui/tabs/README.md +0 -141
  185. package/src/ui/tabs/primitives.ts +0 -2
  186. package/src/ui/tabs.ts +0 -2
  187. package/src/ui/toggle/README.md +0 -56
  188. package/src/ui/toggle/primitives.ts +0 -2
  189. package/src/ui/toggle.ts +0 -3
  190. package/src/ui-hmr.ts +0 -2
  191. package/src/ui.ts +0 -2
@@ -17,7 +17,7 @@ A Remix component is a setup function that returns a render function. Setup runs
17
17
  creates the component. Render runs immediately after setup and again whenever the component updates.
18
18
 
19
19
  ```tsx filename=app/ui/album-heading.tsx
20
- import type { Handle } from "remix/ui";
20
+ import type { Handle } from "remix/component";
21
21
 
22
22
  type AlbumHeadingProps = {
23
23
  artist: string;
@@ -48,7 +48,7 @@ components, strings, numbers, booleans, `null`, `undefined`, and nested arrays o
48
48
  Fragments let you return siblings without adding another DOM element:
49
49
 
50
50
  ```tsx filename=app/ui/album-byline.tsx
51
- import type { Handle } from "remix/ui";
51
+ import type { Handle } from "remix/component";
52
52
 
53
53
  export function AlbumByline(handle: Handle<{ artist: string; year: number }>) {
54
54
  return () => (
@@ -64,8 +64,8 @@ The same component can render to HTML on the server, mount into a client-only ro
64
64
  a server-rendered page. Only the last two cases execute it in the browser.
65
65
 
66
66
  ```tsx
67
- import { css, on } from "remix/ui";
68
- import type { Handle } from "remix/ui";
67
+ import { css, on } from "remix/component";
68
+ import type { Handle } from "remix/component";
69
69
 
70
70
  export function ComponentModelDemo() {
71
71
  return () => (
@@ -142,7 +142,7 @@ Destructuring the object is safe. Destructuring one of its properties in setup c
142
142
  initial value.
143
143
 
144
144
  ```tsx
145
- import type { Handle } from "remix/ui";
145
+ import type { Handle } from "remix/component";
146
146
 
147
147
  function AlbumTitle(handle: Handle<{ title: string }>) {
148
148
  let { props } = handle;
@@ -156,8 +156,8 @@ Local state is ordinary JavaScript declared in setup scope. Store values that af
156
156
  derive everything else inside render:
157
157
 
158
158
  ```tsx filename=app/ui/album-list.tsx
159
- import { on } from "remix/ui";
160
- import type { Handle } from "remix/ui";
159
+ import { on } from "remix/component";
160
+ import type { Handle } from "remix/component";
161
161
 
162
162
  type Album = {
163
163
  id: string;
@@ -203,7 +203,7 @@ list changes order. [Interactivity](/interactivity/) covers event handlers, upda
203
203
  needs to connect a label, input, description, or ARIA relationship without requiring an `id` prop:
204
204
 
205
205
  ```tsx
206
- import type { Handle } from "remix/ui";
206
+ import type { Handle } from "remix/component";
207
207
 
208
208
  function AlbumSearch(handle: Handle) {
209
209
  return () => (
@@ -219,7 +219,7 @@ Use component context when descendants need a value that does not belong on ever
219
219
  component. The provider type is the context key, so `get()` remains typed:
220
220
 
221
221
  ```tsx filename=app/ui/catalog.tsx
222
- import type { Handle, RemixNode } from "remix/ui";
222
+ import type { Handle, RemixNode } from "remix/component";
223
223
 
224
224
  type CatalogContext = {
225
225
  currency: "USD" | "EUR";
@@ -293,8 +293,8 @@ Pages should render a complete document through one shared component. The defaul
293
293
  `app/actions/document.tsx`:
294
294
 
295
295
  ```tsx filename=app/actions/document.tsx
296
- import type { Handle, RemixNode } from "remix/ui";
297
- import { ImportMap } from "remix/ui/server";
296
+ import type { Handle, RemixNode } from "remix/component";
297
+ import { ImportMap } from "remix/component/server";
298
298
 
299
299
  import { scriptEntry } from "../assets.ts";
300
300
 
@@ -347,8 +347,8 @@ Use `css(...)` for static rules. It supports pseudo-selectors, pseudo-elements,
347
347
  attribute selectors, and media queries with normal CSS nesting:
348
348
 
349
349
  ```tsx filename=app/ui/album-card.tsx
350
- import { css } from "remix/ui";
351
- import type { Handle } from "remix/ui";
350
+ import { css } from "remix/component";
351
+ import type { Handle } from "remix/component";
352
352
 
353
353
  const cardStyle = css({
354
354
  border: "1px solid #d6d6d6",
@@ -385,8 +385,8 @@ During server rendering, Remix collects generated rules, deduplicates them, and
385
385
  JavaScript to receive its component styles.
386
386
 
387
387
  ```tsx
388
- import { css } from "remix/ui";
389
- import type { Handle } from "remix/ui";
388
+ import { css } from "remix/component";
389
+ import type { Handle } from "remix/component";
390
390
 
391
391
  export function StylingCardDemo() {
392
392
  return () => (
@@ -519,8 +519,8 @@ const productButtonStyles = css({
519
519
 
520
520
  ## Cascade layers and app-owned design tokens {#theme-tokens-and-cascade-layers}
521
521
 
522
- Generated `css(...)` rules, including styles from first-party UI components, live in the native
523
- `rmx` cascade layer. If your app uses its own layers, declare the complete order once:
522
+ Generated `css(...)` rules live in the native `rmx` cascade layer. If your app uses its own layers,
523
+ declare the complete order once:
524
524
 
525
525
  ```css filename=app/actions/public/app.css
526
526
  @layer base, rmx, app;
@@ -545,309 +545,43 @@ Generated `css(...)` rules, including styles from first-party UI components, liv
545
545
  }
546
546
  ```
547
547
 
548
- Layers before `rmx` provide defaults that Remix component styles can override. Layers after `rmx`
549
- can override component styles deliberately. Unlayered author CSS outranks normal layered CSS, so use
550
- it intentionally when the rest of the app has an explicit layer order.
548
+ Layers before `rmx` provide defaults that generated styles can override. Layers after `rmx` can
549
+ override generated styles deliberately. Unlayered author CSS outranks normal layered CSS, so use it
550
+ intentionally when the rest of the app has an explicit layer order.
551
551
 
552
- Remix supplies behavior and a small set of component styles, not an application theme. Keep brand
553
- colors, spacing, typography, radii, and other design tokens in app-owned CSS custom properties or
554
- TypeScript values.
552
+ Keep brand colors, spacing, typography, radii, and other design tokens in app-owned CSS custom
553
+ properties or TypeScript values.
555
554
 
556
- ## First-party UI building blocks {#first-party-ui-components}
555
+ ## Optional headless UI primitives {#headless-ui-primitives}
557
556
 
558
- The `remix/ui/*` subpaths cover three levels of ownership. Start with the highest-level API whose
559
- markup fits the product, then move down only when the app needs control the composed component does
560
- not expose.
557
+ `@remix-run/ui` is a separate package of headless, accessible interaction primitives. The package
558
+ is currently unstable and versioned independently. It is not available through the `remix` package.
559
+ Install it when an app needs reusable behavior for controls such as accordions, comboboxes,
560
+ listboxes, menus, popovers, selects, tabs, or toggles:
561
561
 
562
- These APIs all render on the server. Controls that handle browser events must also be inside a
563
- `clientEntry(...)` boundary, either directly or through an interactive ancestor. The next chapter
564
- shows how to choose that boundary.
565
-
566
- | Level | Subpaths | What the app owns |
567
- | ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
568
- | Style mixins | `button`, `input`, `checkbox`, `radio`, `toggle` | The native control, its label, layout, and state. |
569
- | Composed controls | `accordion`, `breadcrumbs`, `combobox`, `menu`, `select`, `tabs` | The surrounding page and the values passed into the control. |
570
- | Headless primitives | `popover`, `listbox`, `anchor`, and each available `/primitives` subpath | Markup and styling while Remix supplies focused behavior. |
571
-
572
- The [`remix/ui` API overview](../src/ui/README.md) links to the complete API
573
- for every subpath. The sections below show how to choose among them.
574
-
575
- ### Style mixins for native controls {#style-mixins-keep-native-controls-native}
576
-
577
- Style mixins keep native form behavior in the element you render:
578
-
579
- ```tsx filename=app/ui/album-actions.tsx
580
- import button from "remix/ui/button";
581
- import checkbox from "remix/ui/checkbox";
582
- import { css } from "remix/ui";
583
-
584
- const actionRowStyle = css({
585
- display: "flex",
586
- alignItems: "center",
587
- gap: "0.75rem",
588
- });
589
-
590
- export function AlbumActions() {
591
- return () => (
592
- <div mix={actionRowStyle}>
593
- <button mix={button({ tone: "primary" })}>Add to cart</button>
594
- <label>
595
- <input mix={checkbox()} name="gift" type="checkbox" />
596
- This is a gift
597
- </label>
598
- </div>
599
- );
600
- }
601
- ```
602
-
603
- The checkbox remains a checkbox, participates in `FormData`, and gets its keyboard behavior from the
604
- browser. The mixin supplies visuals. Compose an array in `mix` when a host also needs app-owned styles
605
- or behavior.
606
-
607
- ```tsx
608
- import { css } from "remix/ui";
609
- import button from "remix/ui/button";
610
-
611
- /**
612
- * @name Button Basic
613
- * @description The button mixin applies neutral, primary, or ghost pill styling to button-like hosts.
614
- * @layout center
615
- */
616
- export function ButtonBasic() {
617
- return () => (
618
- <div mix={buttonDemoCss}>
619
- <section mix={toneSectionCss}>
620
- <h2 mix={toneLabelCss}>Neutral</h2>
621
- <div mix={buttonRowCss}>
622
- <button mix={button()}>Medium</button>
623
- <button mix={button({ size: "lg" })}>Large</button>
624
- <button aria-pressed="true" mix={button()}>
625
- Pressed
626
- </button>
627
- <button disabled mix={button()}>
628
- Disabled
629
- </button>
630
- </div>
631
- </section>
632
-
633
- <section mix={toneSectionCss}>
634
- <h2 mix={toneLabelCss}>Primary</h2>
635
- <div mix={buttonRowCss}>
636
- <button mix={button({ tone: "primary" })}>Medium</button>
637
- <button mix={button({ size: "lg", tone: "primary" })}>Large</button>
638
- <button aria-pressed="true" mix={button({ tone: "primary" })}>
639
- Pressed
640
- </button>
641
- <button disabled mix={button({ tone: "primary" })}>
642
- Disabled
643
- </button>
644
- </div>
645
- </section>
646
-
647
- <section mix={toneSectionCss}>
648
- <h2 mix={toneLabelCss}>Ghost</h2>
649
- <div mix={buttonRowCss}>
650
- <button mix={button({ tone: "ghost" })}>Medium</button>
651
- <button mix={button({ size: "lg", tone: "ghost" })}>Large</button>
652
- <button aria-pressed="true" mix={button({ tone: "ghost" })}>
653
- Pressed
654
- </button>
655
- <button disabled mix={button({ tone: "ghost" })}>
656
- Disabled
657
- </button>
658
- </div>
659
- </section>
660
- </div>
661
- );
662
- }
663
-
664
- const buttonDemoCss = css({
665
- display: "grid",
666
- gap: "22px",
667
- width: "min(100%, 34rem)",
668
- });
669
-
670
- const toneSectionCss = css({
671
- display: "grid",
672
- gap: "8px",
673
- });
674
-
675
- const toneLabelCss = css({
676
- margin: 0,
677
- fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
678
- fontSize: "11px",
679
- lineHeight: "14px",
680
- fontWeight: 600,
681
- letterSpacing: 0,
682
- color: "rgba(16, 16, 16, 0.58)",
683
- });
684
-
685
- const buttonRowCss = css({
686
- display: "flex",
687
- flexWrap: "wrap",
688
- alignItems: "center",
689
- gap: "10px",
690
- });
691
- ```
692
-
693
- ### Composed controls for common interactions {#composed-components-cover-common-product-ui}
694
-
695
- Composed controls own the relationships among several elements. For example, `Accordion` connects
696
- triggers to content regions and manages disclosure state:
697
-
698
- ```tsx filename=app/ui/shipping-details.tsx
699
- import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "remix/ui/accordion";
700
-
701
- export function ShippingDetails() {
702
- return () => (
703
- <Accordion defaultValue="returns">
704
- <AccordionItem value="delivery">
705
- <AccordionTrigger>Delivery</AccordionTrigger>
706
- <AccordionContent>Ships in two business days.</AccordionContent>
707
- </AccordionItem>
708
- <AccordionItem value="returns">
709
- <AccordionTrigger>Returns</AccordionTrigger>
710
- <AccordionContent>Return unopened albums within 30 days.</AccordionContent>
711
- </AccordionItem>
712
- </Accordion>
713
- );
714
- }
562
+ ```sh
563
+ npm i @remix-run/ui
715
564
  ```
716
565
 
717
- Use a `default*` prop when the control should own its initial state. Use the corresponding controlled
718
- prop and change callback when the parent must own that state. The exact names differ by component:
719
- for example, accordion uses `defaultValue`/`value`, while tabs uses
720
- `defaultActiveTab`/`activeTab`.
721
-
722
- Selection and disclosure events bubble where a component's API documents them, so a parent can
723
- observe changes without threading a callback through every item. Controls that accept `name`, such
724
- as `Select` and `Combobox`, render a hidden input so the selected value participates in a normal form.
725
- Disabled state, accessible names, focus movement, and keyboard behavior remain part of each
726
- component's contract.
566
+ Import each primitive from its package subpath. For example:
727
567
 
728
568
  ```tsx
729
- import { css } from "remix/ui";
730
- import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "remix/ui/accordion";
731
-
732
- /**
733
- * @name Accordion Overview
734
- * @description A single-open disclosure list that keeps settings, billing, or notification rules in one calm section.
735
- * @layout center
736
- * @order 1
737
- */
738
- export function AccordionOverview() {
739
- return () => (
740
- <Accordion defaultValue="account">
741
- <AccordionItem value="account">
742
- <AccordionTrigger>Account defaults</AccordionTrigger>
743
- <AccordionContent>
744
- <p mix={bodyTextCss}>
745
- Keep billing contacts, email summaries, and workspace naming rules in one calm
746
- disclosure list without adding another card layer.
747
- </p>
748
- </AccordionContent>
749
- </AccordionItem>
750
-
751
- <AccordionItem value="billing">
752
- <AccordionTrigger>Billing schedule</AccordionTrigger>
753
- <AccordionContent>
754
- <p mix={bodyTextCss}>
755
- Review invoice timing, payment methods, and renewal reminders with the same spacing and
756
- typography used elsewhere in the system.
757
- </p>
758
- </AccordionContent>
759
- </AccordionItem>
760
-
761
- <AccordionItem value="notifications">
762
- <AccordionTrigger>Notification rules</AccordionTrigger>
763
- <AccordionContent>
764
- <p mix={bodyTextCss}>
765
- Use single mode when only one details panel should stay open at a time in a compact
766
- settings or details view.
767
- </p>
768
- </AccordionContent>
769
- </AccordionItem>
770
- </Accordion>
771
- );
772
- }
773
-
774
- const bodyTextCss = css({
775
- margin: 0,
776
- fontSize: "13px",
777
- lineHeight: "1.65",
778
- color: "#4f4f4f",
779
- });
569
+ import { css } from "remix/component";
570
+ import * as accordion from "@remix-run/ui/accordion";
780
571
  ```
781
572
 
782
- ```tsx
783
- import { css } from "remix/ui";
784
- import { Option, Select } from "remix/ui/select";
785
-
786
- /**
787
- * @name Select Overview
788
- * @description A styled select control with a searchable dropdown and accessible label.
789
- * @layout center
790
- */
791
- export function SelectOverview() {
792
- return () => (
793
- <div mix={stackCss}>
794
- <label for="fruit-select" mix={labelCss}>
795
- Choose a fruit
796
- </label>
797
- <Select
798
- id="fruit-select"
799
- defaultLabel="Banana"
800
- defaultValue="banana"
801
- name="fruit"
802
- mix={selectCss}
803
- >
804
- <Option label="Apple" value="apple" />
805
- <Option label="Apricot" value="apricot" />
806
- <Option label="Banana" value="banana" />
807
- <Option label="Blackberry" value="blackberry" />
808
- <Option label="Blackcurrant" value="blackcurrant" />
809
- <Option label="Blueberry" value="blueberry" />
810
- <Option label="Boysenberry" value="boysenberry" />
811
- <Option label="Cantaloupe" value="cantaloupe" />
812
- </Select>
813
- </div>
814
- );
815
- }
816
-
817
- const selectCss = css({
818
- width: "16rem",
819
- });
820
-
821
- const stackCss = css({
822
- display: "flex",
823
- flexDirection: "column",
824
- gap: "8px",
825
- width: "100%",
826
- });
827
-
828
- const labelCss = css({
829
- margin: 0,
830
- fontSize: "12px",
831
- fontWeight: "600",
832
- color: "#151515",
833
- });
834
- ```
835
-
836
- ### Headless primitives for custom markup {#primitives-keep-behavior-reusable-when-markup-changes}
837
-
838
- Reach for primitives when the product requires different markup, not merely different colors or
839
- spacing. `popover` supplies anchored-surface behavior, `listbox` supplies option highlighting and
840
- selection, and `anchor` handles lower-level floating-element placement. Accordion, combobox, menu,
841
- select, tabs, and toggle also expose `/primitives` subpaths.
573
+ The primitives supply structural attributes, keyboard interaction, focus management, and state
574
+ coordination. Your app supplies the markup and all visual styling. Controls that handle browser
575
+ events must also be inside a `clientEntry(...)` boundary, either directly or through an interactive
576
+ ancestor. The next chapter shows how to choose that boundary.
842
577
 
843
- The primitives are smaller, but the app takes on more responsibility. Preserve native elements when
844
- they fit, keep labels and ARIA relationships intact, and test pointer and keyboard behavior. A custom
845
- select, for example, still needs a provider, trigger, popover, list, options, and hidden input if it
846
- participates in a form.
578
+ Preserve native elements when they fit, keep labels and ARIA relationships intact, and test pointer
579
+ and keyboard behavior. A custom select, for example, still needs a provider, trigger, popover,
580
+ list, options, and hidden input if it participates in a form.
847
581
 
848
582
  ```tsx
849
- import { css } from "remix/ui";
850
- import * as accordion from "remix/ui/accordion/primitives";
583
+ import { css } from "remix/component";
584
+ import * as accordion from "@remix-run/ui/accordion";
851
585
 
852
586
  /**
853
587
  * @name Accordion Primitives
@@ -18,7 +18,7 @@ Begin with links and forms that already describe the request the server should r
18
18
  form has a real action and method:
19
19
 
20
20
  ```tsx filename=app/actions/albums/edit/public/album-edit-form.tsx
21
- import type { Handle } from "remix/ui";
21
+ import type { Handle } from "remix/component";
22
22
 
23
23
  import { routes } from "../../../../routes.ts";
24
24
 
@@ -72,8 +72,8 @@ it needs rather than moving the whole page into browser code.
72
72
  can remain server-only while its form hydrates:
73
73
 
74
74
  ```tsx filename=app/actions/albums/edit/public/album-edit-form.tsx lines=[1,13-15]
75
- import { clientEntry } from "remix/ui";
76
- import type { Handle } from "remix/ui";
75
+ import { clientEntry } from "remix/component";
76
+ import type { Handle } from "remix/component";
77
77
 
78
78
  import { routes } from "../../../../routes.ts";
79
79
 
@@ -123,7 +123,7 @@ import {
123
123
  importModule,
124
124
  preloadShim,
125
125
  } from "remix/multiple-import-maps-polyfill";
126
- import { run } from "remix/ui";
126
+ import { run } from "remix/component";
127
127
 
128
128
  let app = run({
129
129
  async loadModule(moduleUrl, exportName) {
@@ -163,8 +163,8 @@ The returned app runtime has three lifecycle methods:
163
163
  `run()` hydrates the server-rendered page. It is not a second application router. Browser requests
164
164
  still go to the route actions that own the corresponding server behavior.
165
165
 
166
- [Streaming UI with Frames](/streaming-ui-with-frames/) adds the optional `resolveFrame` callback when
167
- the app needs route-owned regions that load or reload independently.
166
+ [Streaming UI with Frames](/streaming-ui-with-frames/) uses the default browser resolver for
167
+ route-owned regions that load or reload independently.
168
168
 
169
169
  ## Mount client-only UI with createRoot {#client-only-roots}
170
170
 
@@ -172,8 +172,8 @@ Use `createRoot(container)` when there is no server-rendered component to hydrat
172
172
  imperative widget mounted into a container created by another application.
173
173
 
174
174
  ```tsx filename=app/actions/public/support-widget.tsx
175
- import { createRoot } from "remix/ui";
176
- import type { Handle } from "remix/ui";
175
+ import { createRoot } from "remix/component";
176
+ import type { Handle } from "remix/component";
177
177
 
178
178
  function SupportWidget(_handle: Handle) {
179
179
  return () => <a href="/support">Contact support</a>;
@@ -200,8 +200,8 @@ Component-local UI state lives in setup scope. Change it in an event handler, th
200
200
  `handle.update()`:
201
201
 
202
202
  ```tsx
203
- import { on } from "remix/ui";
204
- import type { Handle } from "remix/ui";
203
+ import { on } from "remix/component";
204
+ import type { Handle } from "remix/component";
205
205
 
206
206
  function Counter(handle: Handle<{ initialCount: number }>) {
207
207
  let count = handle.props.initialCount;
@@ -233,8 +233,8 @@ throws an error. If rendering discovers work that should run after the commit, s
233
233
  with `handle.queueTask()`.
234
234
 
235
235
  ```tsx
236
- import { on, ref } from "remix/ui";
237
- import type { Handle } from "remix/ui";
236
+ import { on, ref } from "remix/component";
237
+ import type { Handle } from "remix/component";
238
238
 
239
239
  function RenameButton(handle: Handle) {
240
240
  let editing = false;
@@ -294,7 +294,7 @@ the list is sorted or a random value created during render. Matching keys let Re
294
294
  and component instances instead of pairing state with the wrong item.
295
295
 
296
296
  ```tsx
297
- import { on, type Handle } from "remix/ui";
297
+ import { on, type Handle } from "remix/component";
298
298
 
299
299
  type ListItem = {
300
300
  id: string;
@@ -398,8 +398,8 @@ component when an application model changes.
398
398
  `currentTarget` is inferred from that host:
399
399
 
400
400
  ```tsx
401
- import { on } from "remix/ui";
402
- import type { Handle } from "remix/ui";
401
+ import { on } from "remix/component";
402
+ import type { Handle } from "remix/component";
403
403
 
404
404
  function AlbumForm(_handle: Handle) {
405
405
  return () => (
@@ -436,8 +436,8 @@ value. A controlled input gets its current value from component state instead. I
436
436
  copies the DOM value into that state and updates the component:
437
437
 
438
438
  ```tsx filename=app/ui/public/title-inputs.tsx
439
- import { on } from "remix/ui";
440
- import type { Handle } from "remix/ui";
439
+ import { on } from "remix/component";
440
+ import type { Handle } from "remix/component";
441
441
 
442
442
  const initialTitle = "Thriller";
443
443
 
@@ -489,17 +489,22 @@ The `mix` prop attaches reusable behavior and styles to host elements. Pass one
489
489
  array when the element needs several:
490
490
 
491
491
  ```tsx
492
- import button from "remix/ui/button";
493
- import { attrs, css, on, ref } from "remix/ui";
494
- import type { Handle } from "remix/ui";
495
-
496
- const saveButtonStyle = css({ minWidth: "8rem" });
492
+ import { attrs, css, on, ref } from "remix/component";
493
+ import type { Handle } from "remix/component";
494
+
495
+ const saveButtonStyle = css({
496
+ minWidth: "8rem",
497
+ minHeight: "2.5rem",
498
+ border: 0,
499
+ borderRadius: "999px",
500
+ background: "#111",
501
+ color: "#fff",
502
+ });
497
503
 
498
504
  function SaveButton(_handle: Handle) {
499
505
  return () => (
500
506
  <button
501
507
  mix={[
502
- button({ tone: "primary" }),
503
508
  saveButtonStyle,
504
509
  attrs({ type: "button" }),
505
510
  ref((node) => console.log("Mounted", node)),
@@ -530,8 +535,8 @@ recreating static mixins for every state change.
530
535
  observers and element-owned listeners to that signal:
531
536
 
532
537
  ```tsx
533
- import { ref } from "remix/ui";
534
- import type { Handle } from "remix/ui";
538
+ import { ref } from "remix/component";
539
+ import type { Handle } from "remix/component";
535
540
 
536
541
  function MeasuredPanel(handle: Handle) {
537
542
  let width = 0;
@@ -565,8 +570,8 @@ native `addEventListener()`. Schedule browser-only setup after the client commit
565
570
  `handle.signal` so the listener is removed when the component disconnects:
566
571
 
567
572
  ```tsx
568
- import { clientEntry } from "remix/ui";
569
- import type { Handle } from "remix/ui";
573
+ import { clientEntry } from "remix/component";
574
+ import type { Handle } from "remix/component";
570
575
 
571
576
  export const ViewportWidth = clientEntry(import.meta.url, function ViewportWidth(handle: Handle) {
572
577
  let width: number | undefined;
@@ -610,8 +615,8 @@ the signal. In this example, the server passes `routes.albums.search.href()` as
610
615
  route returns a short text result. The browser component does not hard-code the endpoint:
611
616
 
612
617
  ```tsx filename=app/ui/public/album-search.tsx
613
- import { clientEntry, on } from "remix/ui";
614
- import type { Handle } from "remix/ui";
618
+ import { clientEntry, on } from "remix/component";
619
+ import type { Handle } from "remix/component";
615
620
 
616
621
  export const AlbumSearch = clientEntry(
617
622
  import.meta.url,
@@ -674,7 +679,7 @@ operations in ordinary TypeScript that the component calls.
674
679
  A small model makes that boundary concrete. This cart owns its items and quantity changes:
675
680
 
676
681
  ```ts filename=app/ui/public/cart-model.ts
677
- import { TypedEventTarget } from "remix/ui";
682
+ import { TypedEventTarget } from "remix/component";
678
683
 
679
684
  export interface CartItem {
680
685
  id: string;
@@ -711,8 +716,8 @@ The component creates the model from serializable props, listens for changes, an
711
716
  open state for its details panel:
712
717
 
713
718
  ```tsx filename=app/ui/public/cart.tsx
714
- import { clientEntry, on } from "remix/ui";
715
- import type { Handle } from "remix/ui";
719
+ import { clientEntry, on } from "remix/component";
720
+ import type { Handle } from "remix/component";
716
721
 
717
722
  import { CartModel, type CartItem } from "./cart-model.ts";
718
723
 
@@ -763,7 +768,7 @@ navigations through the browser's Navigation API. Cross-origin links, downloads,
763
768
  browser cannot intercept continue as normal document requests.
764
769
 
765
770
  ```tsx filename=app/ui/album-link.tsx
766
- import type { Handle } from "remix/ui";
771
+ import type { Handle } from "remix/component";
767
772
 
768
773
  import { routes } from "../routes.ts";
769
774
 
@@ -778,8 +783,8 @@ Use `navigate(href, options)` when navigation begins in code. Apply `link(href,
778
783
  host that should behave like a link, such as a card whose whole surface is interactive:
779
784
 
780
785
  ```tsx filename=app/ui/album-navigation.tsx
781
- import { link, navigate, on } from "remix/ui";
782
- import type { Handle } from "remix/ui";
786
+ import { link, navigate, on } from "remix/component";
787
+ import type { Handle } from "remix/component";
783
788
 
784
789
  import { routes } from "../routes.ts";
785
790
 
@@ -916,7 +921,7 @@ typed event for consumers.
916
921
  This `longPress()` mixin turns a held pointer into an `app:longpress` event:
917
922
 
918
923
  ```tsx filename=app/ui/public/long-press.tsx
919
- import { createMixin, on } from "remix/ui";
924
+ import { createMixin, on } from "remix/component";
920
925
 
921
926
  export const longPressType = "app:longpress" as const;
922
927
 
@@ -979,7 +984,7 @@ Consume the semantic event with `on(...)` like any other DOM event:
979
984
  ```
980
985
 
981
986
  Namespace custom event names to avoid collisions, and keep one-off behavior in the component that
982
- uses it. The [`remix/ui` API overview](../src/ui/README.md) covers the full
987
+ uses it. The [`remix/component` API overview](../src/component/README.md) covers the full
983
988
  mixin API.
984
989
 
985
990
  The next chapter uses the same server renderer and browser runtime to stream and reload route-owned