Theming

Shim ships with a theme system that keeps tokens, variants, and layout patterns in sync across all components. The examples below reuse the Field component playground to demonstrate how the same controls adapt across different surfaces.

Size

All components support four sizes that scale proportionally across components.

size: 1
size: 2
size: 3
size: 4
Size example
import { ListIcon } from "@phosphor-icons/react/dist/ssr";
import { Button } from "@/shim-ui/button";
import { ICON_SIZE_MAP } from "@/shim-ui/lib/theme";
import { Menu, MenuItem, MenuSeparator, MenuTrigger } from "@/shim-ui/menu";
import { Slider } from "@/shim-ui/slider";
import { Switch } from "@/shim-ui/switch";
import { TextField } from "@/shim-ui/text-field";
import { ToggleButton } from "@/shim-ui/toggle-button";
import { ToggleButtonGroup } from "@/shim-ui/toggle-button-group";

export default () => (
  <>
    {/* biome-ignore lint/style/noMagicNumbers: no magic */}
    {([1, 2, 3, 4] as const).map((size) => (
      <div className="flex flex-col gap-3" key={size}>
        <code className="block font-medium">size: {size}</code>
        <div className="flex gap-2">
          <Button className="flex-1" size={size}>
            Button
          </Button>
          <MenuTrigger>
            <Button aria-label="Menu" size={size}>
              <ListIcon size={ICON_SIZE_MAP[size]} />
            </Button>
            <Menu size={size}>
              <MenuItem>Edit</MenuItem>
              <MenuItem>Duplicate</MenuItem>
              <MenuSeparator />
              <MenuItem intent="danger">Delete</MenuItem>
            </Menu>
          </MenuTrigger>
        </div>
        <ToggleButtonGroup size={size}>
          <ToggleButton id="1">Left</ToggleButton>
          <ToggleButton id="3">Right</ToggleButton>
        </ToggleButtonGroup>
        <TextField aria-label="TextField" defaultValue="Hello" size={size} />

        <div className="flex gap-3">
          <Slider aria-label="Slider" className="flex-1" isFilled size={size} />
          <Switch aria-label="Switch" size={size} />
        </div>
      </div>
    ))}
  </>
);

Variants

Field variants

Forms and field-based components support classic, soft and outline variants that adjust visual styles while preserving spacing and layout.

Classic
Soft
Outline
Example
import { Checkbox, CheckboxGroup } from "@/shim-ui/checkbox";
import { NumberField } from "@/shim-ui/number-field";
import { Radio, RadioGroup } from "@/shim-ui/radio-group";
import { SearchField } from "@/shim-ui/search-field";
import { Slider } from "@/shim-ui/slider";
import { Switch } from "@/shim-ui/switch";
import { TextField } from "@/shim-ui/text-field";

export default () => (
  <>
    <div>
      <h5 className="text-neutral-text!">Classic</h5>
      <div className="grid grid-cols-4 gap-4">
        <TextField className="col-span-2" defaultValue="Hello" />
        <NumberField className="col-span-2" defaultValue={0} />
        <SearchField className="col-span-full" />
        <Switch className="col-span-1" defaultSelected />
        <Slider className="col-span-3" defaultValue={50} isFilled />
        <RadioGroup className="col-span-2" defaultValue="1">
          <Radio value="1">Option 1</Radio>
          <Radio value="2">Option 2</Radio>
        </RadioGroup>
        <CheckboxGroup className="col-span-2" defaultValue={["1"]}>
          <Checkbox value="1">Option 1</Checkbox>
          <Checkbox value="2">Option 2</Checkbox>
        </CheckboxGroup>
      </div>
    </div>

    <div>
      <h5 className="text-neutral-text!">Soft</h5>
      <div className="grid grid-cols-4 gap-4">
        <TextField className="col-span-2" defaultValue="Hello" variant="soft" />
        <NumberField className="col-span-2" defaultValue={0} variant="soft" />
        <SearchField className="col-span-full" variant="soft" />
        <Switch className="col-span-1" defaultSelected variant="soft" />
        <Slider
          className="col-span-3"
          defaultValue={50}
          isFilled
          variant="soft"
        />
        <RadioGroup className="col-span-2" defaultValue="1" variant="soft">
          <Radio value="1">Option 1</Radio>
          <Radio value="2">Option 2</Radio>
        </RadioGroup>
        <CheckboxGroup
          className="col-span-2"
          defaultValue={["1"]}
          variant="soft"
        >
          <Checkbox value="1">Option 1</Checkbox>
          <Checkbox value="2">Option 2</Checkbox>
        </CheckboxGroup>
      </div>
    </div>

    <div>
      <h5 className="text-neutral-text!">Outline</h5>
      <div className="grid grid-cols-4 gap-4">
        <TextField
          className="col-span-2"
          defaultValue="Hello"
          variant="outline"
        />
        <NumberField
          className="col-span-2"
          defaultValue={0}
          variant="outline"
        />
        <SearchField className="col-span-full" variant="outline" />
        <Switch className="col-span-1" defaultSelected variant="outline" />
        <Slider
          className="col-span-3"
          defaultValue={50}
          isFilled
          variant="outline"
        />
        <RadioGroup className="col-span-2" defaultValue="1" variant="outline">
          <Radio value="1">Option 1</Radio>
          <Radio value="2">Option 2</Radio>
        </RadioGroup>
        <CheckboxGroup
          className="col-span-2"
          defaultValue={["1"]}
          variant="outline"
        >
          <Checkbox value="1">Option 1</Checkbox>
          <Checkbox value="2">Option 2</Checkbox>
        </CheckboxGroup>
      </div>
    </div>
  </>
);

Button variants

Buttons support soft, solid, and ghost variants.

Soft
Solid
Ghost
Example
import { PushPinIcon } from "@phosphor-icons/react/dist/ssr";
import { Button } from "@/shim-ui/button";
import { Theme } from "@/shim-ui/lib/theme";
import { ToggleButton } from "@/shim-ui/toggle-button";
import { ToggleButtonGroup } from "@/shim-ui/toggle-button-group";

export default () => (
  <>
    <div>
      <h5 className="text-neutral-text!">Soft</h5>
      <div className="grid grid-cols-[1fr_auto] gap-4">
        <Button>Hello</Button>
        <ToggleButton>
          <PushPinIcon size={16} />
        </ToggleButton>
        <ToggleButtonGroup
          className="col-span-full"
          defaultSelectedKeys={[1]}
          selectionMode="single"
        >
          <ToggleButton id={1}>
            <PushPinIcon size={16} />
          </ToggleButton>
          <ToggleButton id={2}>
            <PushPinIcon size={16} />
          </ToggleButton>
          <ToggleButton id={3}>
            <PushPinIcon size={16} />
          </ToggleButton>
        </ToggleButtonGroup>
      </div>
    </div>

    <div>
      <h5 className="text-neutral-text!">Solid</h5>
      <Theme buttonVariant="solid">
        <div className="grid grid-cols-[1fr_auto] gap-4">
          <Button>Hello</Button>
          <ToggleButton>
            <PushPinIcon size={16} />
          </ToggleButton>
          <ToggleButtonGroup
            className="col-span-full"
            defaultSelectedKeys={[1]}
            selectionMode="single"
          >
            <ToggleButton id={1}>
              <PushPinIcon size={16} />
            </ToggleButton>
            <ToggleButton id={2}>
              <PushPinIcon size={16} />
            </ToggleButton>
            <ToggleButton id={3}>
              <PushPinIcon size={16} />
            </ToggleButton>
          </ToggleButtonGroup>
        </div>
      </Theme>
    </div>

    <div>
      <h5 className="text-neutral-text!">Ghost</h5>
      <Theme buttonVariant="ghost">
        <div className="grid grid-cols-[1fr_auto] gap-4">
          <Button>Hello</Button>
          <ToggleButton>
            <PushPinIcon size={16} />
          </ToggleButton>
          <ToggleButtonGroup
            className="col-span-full"
            defaultSelectedKeys={[1]}
            selectionMode="single"
          >
            <ToggleButton id={1}>
              <PushPinIcon size={16} />
            </ToggleButton>
            <ToggleButton id={2}>
              <PushPinIcon size={16} />
            </ToggleButton>
            <ToggleButton id={3}>
              <PushPinIcon size={16} />
            </ToggleButton>
          </ToggleButtonGroup>
        </div>
      </Theme>
    </div>
  </>
);

Set global defaults

Wrap your application (or any subtree) in the <Theme> provider to broadcast shared props to every Shim component underneath it. The provider merges values from the nearest ancestor, so you can layer overrides as needed.

layout.tsx
import type { ReactNode } from "react";
import { Theme } from "@/shim-ui/lib/theme";
import "./globals.css";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Theme fieldVariant="soft" labelPosition="side" size={2}>
          {children}
        </Theme>
      </body>
    </html>
  );
}

Inside a page or component you can scope a more specific override while inheriting the rest of the defaults:

components/callout.tsx
import { Theme } from "@/shim-ui/lib/theme";
import { Button } from "@/shim-ui/button";

export function Callout() {
  return (
    <Theme buttonVariant="solid">
      <Button>Download</Button>
    </Theme>
  );
}

Theme API reference

size

1 | 2 | 3 | 4

1

Sets the base size for all components. Sizes scale proportionally across components.

labelPosition

"side" | "top"

"side"

Sets the default position for labels on field-based components.

fieldVariant

"classic" | "soft" | "outline"