# Remix

> Set up the React color picker in Remix or React Router v7: install it, link the stylesheet from a links export, and render it in a route.

Source: https://chroma-panel.jscrate.dev/react/frameworks/remix
Last updated: 2026-09-18

chroma-panel works as a React color picker for Remix with no wrapper, no
`ClientOnly` and no `typeof window` guard. This page adds the stylesheet through
a `links` export and renders the picker in a route. The same steps apply to
React Router v7 in framework mode, which is the same code under a new name.

## Install

```bash
npm install chroma-panel
```

The package has no dependencies. `react` and `react-dom` are peer dependencies,
so the copies your app already has are the ones that get used.

## Link the stylesheet

The panel injects its stylesheet from an effect, which leaves the
server-rendered markup unstyled until hydration. Remix has its own way to load
CSS: import the file's URL and return it from a `links` export.

```tsx
// app/root.tsx
import type { LinksFunction } from "@remix-run/node";
import chromaPanelStyles from "chroma-panel/style.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: chromaPanelStyles },
];
```

On React Router v7, the type comes from `react-router` instead:

```tsx
import type { LinksFunction } from "react-router";
import chromaPanelStyles from "chroma-panel/style.css?url";
```

Then pass `injectStyles={false}` wherever you render the panel, so it does not
write a second copy into a `<style>` tag.

> **?url is a Vite feature, not a Remix one**
>
> Vite resolves the specifier with the query stripped, then hands you the built
> file's URL. `chroma-panel` lists `"./style.css"` in its `exports` map, so the
> subpath resolves to `dist/style.css`. It works in Remix v2 and React Router v7
> because both build with Vite. On a build that does not use Vite, drop the
> `?url` and load the CSS some other way.

If your app uses Tailwind, the layer order matters more than the loading
mechanism — [styling with Tailwind](https://chroma-panel.jscrate.dev/react/handbook/tailwind) has the import
order that keeps your utility classes winning.

## A route

```tsx
// app/routes/brand.tsx
import { useState } from "react";
import { ColorInput, type ColorChangeResult } from "chroma-panel";

export default function BrandRoute() {
  const [color, setColor] = useState<string>("#3366cc");

  const handleChange = (result: ColorChangeResult): void => {
    setColor(result.hex);
  };

  return (
    <main>
      <h2>Brand color</h2>
      <ColorInput value={color} onChange={handleChange} injectStyles={false} />
      <p style={{ color }}>{color}</p>
    </main>
  );
}
```

To post the color to an action instead of holding it in state, give
`ColorInput` a `name` and drop `value` and `onChange`. It renders a hidden input
and submits with the form like any other control, as
[forms](https://chroma-panel.jscrate.dev/react/handbook/forms) describes.

## Server rendering

Nothing needs guarding. The route above renders on the server as it stands:

- Every file in the package that touches the browser carries `'use client'`, and
  nothing is read from `window` at module scope.
- `injectStyles` returns early when `document` is undefined, so the server pass
  never tries to create a `<style>` tag.
- The first render writes the color as CSS custom properties on the panel's root
  element, so the server-rendered markup already carries the right color and
  there is no flash before hydration.

[Server rendering](https://chroma-panel.jscrate.dev/react/handbook/server-rendering) covers the same points for
Next.js and other SSR setups, and
[entry points](https://chroma-panel.jscrate.dev/react/utils/entry-points) lists the subpaths if you want to ship
fewer modes than the default import registers.
