Terracotta
GitHub

State attributes

Every component stamps a marker attribute named after itself, such as tc-dialog, tc-accordion-button or tc-listbox-option. On top of that, it writes whichever of these apply:

Attribute Values Meaning
tc-disabled "" when disabled, otherwise absent Interaction is blocked. Focus navigation also uses it to skip the element.
tc-expanded "" when open Disclosure-like state is open.
tc-checked "" when checked, "mixed" when indeterminate, absent when unchecked Checkbox / radio state.
tc-selected "" when selected Option, tab or accordion item is the selected value.
tc-active "" when active Element currently holds focus within its group.
tc-pressed "" when pressed Toggle button state.
tc-matches "" when the option matches the query Combobox/Command filtering.
tc-has-selected "" A descendant is selected.
tc-has-active "" A descendant is active.
tc-has-query "" The autocomplete query is non-empty.
tc-owner owner id Ties a focusable descendant to the root that navigates it. Internal, but present in the DOM.
tc-transition enter-from | enter-to | entered | leave-from | leave-to Current phase of a <TransitionChild>.

Boolean attributes hold an empty string when on, and are absent when off. That is exactly what a bare attribute selector tests for:

[tc-selected] { font-weight: 600; }
[tc-disabled] { opacity: 0.5; pointer-events: none; }

tc-checked is the only one with three states, so match on its value:

[tc-checked=""]      { /* checked */ }
[tc-checked="mixed"] { /* indeterminate */ }
:not([tc-checked])   { /* unchecked */ }

Styling with them

Give the element a class as usual, then combine it with the state attribute. The two never collide, because Terracotta only writes attributes:

<ListboxOption value={person} class="option">
  {person.name}
</ListboxOption>
.option {
  padding: 0.5rem 0.75rem;
  cursor: pointer;
}

/* Keyboard or pointer is on this option */
.option[tc-active] {
  background: #eff6ff;
}

/* This option is the current value */
.option[tc-selected] {
  font-weight: 600;
}

.option[tc-disabled] {
  color: #9ca3af;
  cursor: not-allowed;
}

The tc-has-* attributes sit on a container and describe its descendants. That lets a parent react to its children in plain CSS:

/* Dim the placeholder text on the button until something is chosen */
.listbox-button:not([tc-has-selected]) .value {
  color: #9ca3af;
}

The attributes live on real elements, so they work with ordinary selectors: :hover, :focus-visible, sibling and descendant combinators, media queries. Nothing extra is needed.

Reading them in code

The state behind these attributes is also available to your markup, through a render prop or a hook. You rarely need to read the DOM:

<Disclosure defaultOpen={false}>
  {({ isOpen }) => <DisclosureButton>{isOpen() ? 'Hide' : 'Show'}</DisclosureButton>}
</Disclosure>

Each component page lists both: the attributes it writes, and the state object they come from. See state for how render props and hooks work.