Terracotta
GitHub

Install

npm i terracotta@next
# or
pnpm add terracotta@next
# or
yarn add terracotta@next

Terracotta 2 is published under the next tag while it is a release candidate. The latest tag still points at the 1.x line, which targets Solid 1 and is imported from the package root rather than one entry per component.

solid-js and @solidjs/web are peer dependencies. Terracotta 2 targets Solid 2 (^2.0.0-rc.13).

The published code needs Node 20 or later, and uses syntax up to ES2022, such as class static blocks. A toolchain that cannot parse that has to transpile the package itself.

Imports

Terracotta has no barrel entry point. Every component is its own entry, named after itself, and the state primitives share one:

import { Disclosure } from 'terracotta/disclosure';
import { Listbox } from 'terracotta/listbox';
import { useDisclosureState } from 'terracotta/states';

Each page in this documentation opens with the exact import line for what it covers. Importing a component pulls in that component and nothing else, so a page that uses one widget never pays for the other twenty-two.

Your first component

Every widget is a set of parts that you compose. Terracotta renders nothing beyond the elements you write, and adds no class names to them.

import {
  Disclosure,
  DisclosureButton,
  DisclosurePanel,
} from 'terracotta/disclosure';

function Details() {
  return (
    <Disclosure defaultOpen={false}>
      <DisclosureButton class="trigger">Details</DisclosureButton>
      <DisclosurePanel class="panel">Everything worth knowing.</DisclosurePanel>
    </Disclosure>
  );
}

Those few lines already cover what an accessible disclosure needs. The button carries aria-expanded. While the panel is open, the button points at it with aria-controls. While it is closed, the panel is not in the DOM at all.

The mental model

Four ideas apply to every component in the library:

  • You own the markup. Each part renders a default element. Use as to swap it for another element or your own component. See rendering.
  • You own the state, or the component does. Pass defaultOpen and the component manages itself. Pass isOpen and you drive it. See state.
  • You own the styling. State reaches CSS through tc- attributes, never through class names. See styling.
  • The library owns the accessibility. Roles, ARIA relationships, focus management and keyboard handling come with the component. See keyboard and focus.

The Claude Code plugin

Terracotta ships a plugin for Claude Code. It carries a skill that describes the component catalogue and every part name, the subpath imports, controlled versus uncontrolled state, render props, as and prop passthrough, the tc- attributes, unmount, transitions, and the keyboard and focus behaviour, with a copy of this reference material alongside it, so the detail is available without a fetch.

/plugin marketplace add lxsmnsyc/terracotta
/plugin install terracotta@terracotta

Nothing else needs configuring. The skill declares the situations it applies to, so it loads on its own once you are working in SolidJS code that imports from terracotta, and stays out of the way otherwise. Check that it registered with:

/plugin

It documents Terracotta 2 and marks where 1.x differs, chiefly that 1.x has a package root entry, so import { Dialog } from 'terracotta' is right there and resolves to nothing here.

Where to go next