Extend
Extend builds a new Marmo component on top of an existing component. It preserves the component's props and class logic, then adds classes, dynamic interpolations, logic, or variants.
- Features
- works with framework components and existing Marmo components
- infers the base component's props
- supports additional typed
$props - composes with
logic,variants, andtransform
extend needs an existing component or Marmo element to build upon. Use it when you genuinely want to preserve and add to that component's rendering, props, or class logic.
When creating a new Marmo component, start with a base element such as ma.button. If the component mainly needs reusable styling choices, prefer Variants in most cases.
Extend a component
Pass a component to ma.extend, then add classes with a tagged template. In React, the base component must forward className; in Solid, it must forward class.
import ma from '@marmo/react'
const Link = ({ className, ...props }) => <a className={className} {...props} />
const NavigationLink = ma.extend(Link)`
rounded-md
px-3 py-2
font-medium
`
const MyComponent = () => <NavigationLink href="/settings">Settings</NavigationLink>Result
<a href="/settings" class="rounded-md px-3 py-2 font-medium">Settings</a>The original component remains unchanged. Rendering Link elsewhere does not add the classes from NavigationLink. Extending a regular framework component preserves that component's rendering. By contrast, transforming a Marmo component changes its rendered intrinsic element and does not preserve arbitrary framework-component internals.
Add typed props
Provide a type argument to make new props available to interpolations and consumers. Styling props conventionally use a $ prefix and are removed before Marmo renders an intrinsic element.
import ma from '@marmo/react'
const BaseButton = ma.button`
rounded-md px-4 py-2
`
const ActionButton = ma.extend(BaseButton)`
${({ $tone }) => ($tone === 'quiet' ? 'bg-transparent text-neutral' : 'bg-primary text-primary-content')}
${({ $isLoading }) => $isLoading && 'pointer-events-none opacity-60'}
`
const MyComponent = () => (
<ActionButton $tone="primary" $isLoading>
Save
</ActionButton>
)The extended component retains the base button props, such as type, disabled, and event handlers, alongside the new $isLoading and $tone props.
Recipes
Style a third-party component
Props are inferred from a well-typed component. This makes extend useful for styling icons, router links, and headless UI primitives that forward the framework's class prop: className in React or class in Solid.
import { ArrowBigDown } from 'lucide-react'
import ma from '@marmo/react'
const StyledArrow = ma.extend(ArrowBigDown)`
size-5
text-primary
`
const MyComponent = () => <StyledArrow strokeWidth={3} aria-label="Move down" />Extend an extended component
Extensions can be layered. Each layer keeps the classes, interpolations, and prop types from the components below it.
import ma from '@marmo/react'
const Button = ma.button`rounded-md px-4 py-2`
const PrimaryButton = ma.extend(Button)`bg-primary text-primary-content`
const WidePrimaryButton = ma.extend(PrimaryButton)`w-full`
const MyComponent = () => <WidePrimaryButton type="submit">Continue</WidePrimaryButton>Extend with variants
Call .variants() on the extend builder when the new component needs a reusable styling API instead of one-off boolean interpolation logic.
import ma from '@marmo/react'
const BaseButton = ma.button`rounded-md px-4 py-2`
const Button = ma.extend(BaseButton).variants({
variants: {
$size: {
sm: 'h-8 text-sm',
lg: 'h-12 text-lg'
}
},
defaultVariants: {
$size: 'sm'
}
})
const MyComponent = () => <Button $size="lg">Create account</Button>The base component can itself be a variants component. Its original variants continue to work, and the new extension adds another layer of classes or variants.
Derive props with logic
Call .logic() before the template to derive styling props without exposing them to consumers.
import ma from '@marmo/react'
const Progress = ({ value, className }) => (
<div className={className} aria-valuenow={value} role="progressbar" />
)
const StyledProgress = ma.extend(Progress).logic(({ value }) => ({ $isComplete: value >= 100 }))`
h-2 rounded-full
${({ $isComplete }) => ($isComplete ? 'bg-success' : 'bg-primary')}
`
const MyComponent = () => <StyledProgress value={100} />Help TypeScript with loosely typed components
Marmo can only infer the types exposed by the input component. If a library exports a component as any or with overly broad props, create a typed wrapper before extending it.
import ma from '@marmo/react'
import { Field } from 'some-form-library'
const TypedField = (props) => <Field {...props} />
const StyledField = ma.extend(TypedField)`
w-full rounded-md border
${({ $error }) => $error && 'border-error'}
`This wrapper is also the right place to adapt a library-specific class prop to className in React or class in Solid when a component does not forward it directly.
Class merging
Classes from the base component, the extension, and the consumer's class prop are composed by Marmo. When utility classes conflict, the later class wins according to tailwind-merge rules.
const Base = ma.button`px-2 bg-neutral`
const Extended = ma.extend(Base)`px-4 bg-primary`
;<Extended className="px-6" />The final class list uses px-6 and bg-primary while retaining non-conflicting classes.