Lesson 3 of 14

A first component

A Button from the archetype: a contract, two parts, and facets that point at tokens.

#The idea

A component is a contract and an anatomy. The contract is what consumers see: props and events. The anatomy is what the component is made of: parts, each saying what it is, how it is painted and how it lays out. You do not start either from nothing: an archetype gives you both, correct, and you make them yours.

#What this lesson adds

milda
component Button "The primary action." {
  archetype Button

  props {
    label : string = "Get started"
  }

  events {
    click
  }

  part Button: container {
    emits { click on activate }
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    corner radius.sm
    inset space.sm space.lg
  }

  part label: text {
    slot of content default
    content bind label
    text size.md
    weight weight.medium
  }
}

#What to notice

  • archetype Button is most of the work. The prelude's Button composes activation (the action fires on press, Enter and Space) and pressability (pressed and hovered feedback), and it knows the button must have an accessible name. None of that is written here, and all of it is in the generated code.
  • The first part is the root. part Button: container { reads “a part named Button, which is a container”. The word after the colon is the kind: what the part is, never which element it becomes.
  • Facets point at tokens. fill accent, ink on-accent, corner radius.sm, inset space.sm space.lg. A facet is an abstract paint slot: fill is the surface, ink the foreground, inset the padding (block then inline). There is no CSS property anywhere; each target maps a facet to its own primitive.
  • Events are declared, then raised. events { click } is the contract; emits { click on activate } on the root says which interaction raises it. is neutral: click, tap, Enter or Space, as the target sees fit.
  • The label is a slot. slot of content default makes the label part the place a consumer's children go; content bind label gives it the prop when no children are passed.

#What the generator makes of it

A Button.tsx with a props interface, a real <button> (the target decided the element from the kind and the archetype), an onClick wired to the click event, and a CSS module whose values are the custom properties from lesson 1.

#Try it

Change fill accent to fill surface and ink on-accent to ink accent. Then put the fills back and change the accent token instead: the button follows, which is the whole point of lesson 2.

Words in this lesson

  • A component of the library: its contract (props, events), its anatomy (parts), and how it is realized.
  • The built-in blueprint the component is an instance of (Button, Select, Dialog, ...).
  • A node of the component’s anatomy.
  • What the part IS, in neutral terms.
  • A paint facet: an abstract slot such as "background" or "text colour", pointed at a token.
  • The events the component raises.
  • Which declared event this part raises, and on what interaction.
  • The part is a hole the consumer fills: the label of a button, the items of a menu.
  • The text the part shows: fixed, bound to a prop, or chosen by conditions.

Open a word for its entry, or read them all in the reference.

#The file at the end of this lesson

milda
milda 0.10
prelude 0.0.2

meta { name "Acme" }

context-group ColorScheme { Light Dark }

foundations {
  layer Base {
    token violet/100 : color = hex(ede9fe)
    token violet/500 : color = hex(8b5cf6)
    token violet/600 : color = hex(7c3aed)
    token violet/700 : color = hex(6d28d9)
    token violet/900 : color = hex(4c1d95)
    token gray/50 : color = hex(f8fafc)
    token gray/200 : color = hex(e2e8f0)
    token gray/500 : color = hex(64748b)
    token gray/800 : color = hex(1e293b)
    token gray/900 : color = hex(0f172a)
    token green/500 : color = hex(22c55e)
    token amber/500 : color = hex(f59e0b)
    token white : color = hex(ffffff)
  }

  layer Semantic {
    token accent : color = alias by ColorScheme { default -> violet.600; Dark -> violet.500 }
    token accent-hover : color = alias by ColorScheme { default -> violet.700; Dark -> violet.600 }
    token accent-subtle : color = alias by ColorScheme { default -> violet.100; Dark -> violet.900 }
    token on-accent : color = alias by ColorScheme { default -> white }
    token surface : color = alias by ColorScheme { default -> white; Dark -> gray.900 }
    token text : color = alias by ColorScheme { default -> gray.900; Dark -> gray.50 }
    token text-muted : color = alias by ColorScheme { default -> gray.500 }
    token line : color = alias by ColorScheme { default -> gray.200; Dark -> gray.800 }
    token success : color = alias by ColorScheme { default -> green.500 }
    token warning : color = alias by ColorScheme { default -> amber.500 }

    token space/xs : spacing = 4px
    token space/sm : spacing = 8px
    token space/md : spacing = 12px
    token space/lg : spacing = 16px
    token space/xl : spacing = 24px

    token radius/sm : radius = 6px
    token radius/lg : radius = 12px
    token radius/pill : radius = pill

    token hairline : border = 1px

    token size/sm : font-size = 13px
    token size/md : font-size = 15px
    token size/lg : font-size = 18px
    token weight/medium : font-weight = 500
    token weight/bold : font-weight = 700
  }
}

component Button "The primary action." {
  archetype Button

  props {
    label : string = "Get started"
  }

  events {
    click
  }

  part Button: container {
    emits { click on activate }
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    corner radius.sm
    inset space.sm space.lg
  }

  part label: text {
    slot of content default
    content bind label
    text size.md
    weight weight.medium
  }
}