Logic composition with .logic()


Use .logic() to derive props as part of a Marmo component. A logic handler receives the component's current props and may return a partial props object. Marmo shallow-merges that result into the props before it computes template interpolations, inline styles, or variants.

Logic runs whenever the Marmo component renders. You do not need useMemo, an effect, or a second wrapper component just to calculate styling props.

Derive a styling prop

Props prefixed with $ are available to Marmo's styling APIs but are not forwarded to the rendered DOM element. That makes them a good fit for internal state derived by .logic().

react - javascript
import ma from '@marmo/react'
 
const Progress = ma.progress
  .logic(({ value = 0, max = 100 }) => ({
    $isComplete: Number(value) >= Number(max)
  }))
  .variants({
    base: 'h-2 rounded-full',
    variants: {
      $isComplete: {
        true: 'bg-success',
        false: 'bg-base-300'
      }
    }
  })
 
;<Progress value={72} max={100} />

The consumer supplies native progress attributes, so forwarding value and max is intentional. The component owns the definition of “complete.” Keeping that rule next to the styles prevents callers from having to calculate and synchronize both values.

Marmo has no public API for omitting an arbitrary ordinary prop after .logic() runs. $-prefixed props and registered variant props are the supported non-forwarded contracts. If a component needs domain props that must never reach an intrinsic element, consume them in a typed framework wrapper and pass only valid element props plus $ styling props to the Marmo component.

Derive attributes and variants together

A handler can return ordinary element attributes as well as $ props. Ordinary props are forwarded, so the same derived value can drive styling, testing selectors, and accessible state.

react - javascript
import ma from '@marmo/react'
 
const Ticket = ma.button
  .logic(({ disabled }) => {
    const availability = disabled ? 'sold-out' : 'available'
 
    return {
      $availability: availability,
      'data-availability': availability
    }
  })
  .variants({
    base: 'rounded px-4 py-2',
    variants: {
      $availability: {
        available: 'bg-primary text-primary-content',
        'sold-out': 'cursor-not-allowed bg-base-300 text-base-content'
      }
    }
  })
 
;<Ticket disabled>Reserve ticket</Ticket>

$availability is consumed internally and filtered out. data-availability is intentionally rendered, while disabled remains a valid native button prop.

Compose logic in stages

You can chain .logic() calls. They run from left to right, and each handler receives the result of the previous handler. Later results win when the same top-level property is returned more than once.

react - javascript
import ma from '@marmo/react'
 
const Meter = ma.meter
  .logic(({ value, max }) => ({
    $ratio: max > 0 ? value / max : 0
  }))
  .logic((props) => ({
    $level: (props.$ratio ?? 0) >= 0.8 ? 'high' : 'low'
  }))`
    h-2 rounded-full
    ${({ $level }) => ($level === 'high' ? 'bg-warning' : 'bg-success')}
  `

Chaining is helpful when one derived value is reusable or when an extended component adds a new layer of behavior. For a short calculation that is only used once, one handler is usually easier to read.

Because merging is shallow, returning { options: { active: true } } replaces the existing options object; it does not deep-merge its fields.

Updating props

In both React and Solid, logic is evaluated from the current component props. When value, disabled, or another input changes, the derived props and resulting classes are recalculated. Define the Marmo component at module scope and keep changing values in props.

In Solid, preserve prop reactivity in the consuming component by reading props.value instead of destructuring changing props before passing them to the Marmo component.

JavaScript
const Example = (props) => <Meter value={props.value} max={100} />

Type the complete logic contract

The generic passed to .logic<Props>() describes the props the handler can read and return. Include consumer inputs and any derived properties that later handlers, templates, styles, or variants need. Derived properties are commonly optional because consumers do not provide them; .logic() fills them in at render time.

TS
interface NoticeProps {
  errors: readonly string[] // consumer input
  $hasErrors?: boolean      // derived styling prop
}

For variants, keep the derived variant contract explicit with the second generic:

JavaScript
ma.div.variants({/* ... */})

When to use .logic()

.logic() is a strong fit for:

  • deriving $ props used by templates, .style(), or .variants();
  • keeping one domain rule shared between visual state and DOM attributes;
  • normalizing several consumer inputs into a small component state;
  • adding derived behavior while extending an existing Marmo component.

Keep handlers synchronous and deterministic. They run during rendering, so they should calculate and return props—not fetch data, mutate external state, set framework state, or start subscriptions. Use React or Solid primitives for effects, asynchronous work, and state with its own lifecycle.

If the value can be expressed clearly inside a single template interpolation, .logic() may be unnecessary. Reach for it when a derived value is used in multiple places, needs to become an attribute, or meaningfully simplifies the component's public API.