Lesson 4 of 14

Variants and states

A prop that is a dimension, paint per variant, and paint per behaviour state.

#The idea

A button comes in emphases, and it reacts to the pointer and the keyboard. Both are paint that changes under a condition, and Milda has one word for each kind of condition: for a prop, for a behaviour.

#What this lesson adds

A second context group and a prop typed by it, then conditional paint on the root:

milda
context-group Variant { Primary Secondary Ghost }
milda
props {
  variant : context Variant = Primary
  disabled : boolean = false
  label : string = "Get started"
}
milda
bind state disabled disabled
when variant = Secondary {
  fill surface
  ink text
  border line hairline
}
state hovered {
  fill accent-hover
  border accent-hover hairline
}
state hovered when variant = Secondary {
  fill accent-subtle
}
state focus-visible {
  ring accent
}
state disabled {
  opacity 50
}

#What to notice

  • A variant is a dimension, like a colour scheme. variant : context Variant types the prop by the group, so its values are the group's contexts. This is the same mechanism as dark mode: a token can alias by Variant, and the prop supplies the context per instance instead of the environment supplying it.
  • is paint under a prop condition. when variant = Secondary { … } overrides the base paint while the prop has that value. It is not a state: nothing has to happen for it to hold.
  • is paint under a behaviour state. and come from pressability; from the focus the archetype manages; is an ambient gate. You never write a pseudo-class, because a wheel on iOS has no hover and the same block must mean the same thing there.
  • The two combine. state hovered when variant = Secondary is hover paint for one variant only.
  • A state can be driven by a prop. bind state disabled disabled says the part is in the state while the prop is true. The state is still a state; the prop is just where it comes from.
  • is the focus indicator, as a facet. A desktop draws a ring, a phone draws nothing, a television draws a strong highlight. The file says “ring, in the accent” and each target decides.

#What the generator makes of it

The root now carries data-variant, defaulting to "primary", and the stylesheet keys the variant paint on it. The states became the web's own words: :hover, :focus-visible, :disabled. That translation is the generator's, not yours.

css
.button[data-variant="secondary"] {
  background: var(--ds-color-surface);
  color: var(--ds-color-text);
  border-style: solid;
  border-width: var(--ds-borderWidth-hairline);
  border-color: var(--ds-color-line);
}

#Try it

In the preview, force the state and switch the variant through the prop bar. Then add a state pressed block that darkens the fill and see where the generated CSS puts it.

Words in this lesson

  • Paint that applies while a condition holds, with no interaction state involved: the variants of a component.
  • Paint for an interaction state.
  • A prop drives something about the part.
  • A condition.

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 }

context-group Variant { Primary Secondary Ghost }

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, in three emphases." {
  archetype Button

  props {
    variant : context Variant = Primary
    disabled : boolean = false
    label : string = "Get started"
  }

  events {
    click
  }

  part Button: container {
    emits { click on activate }
    bind state disabled disabled
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    border accent hairline
    corner radius.sm
    inset space.sm space.lg
    when variant = Secondary {
      fill surface
      ink text
      border line hairline
    }
    when variant = Ghost {
      fill surface
      ink accent
      border surface hairline
    }
    state hovered {
      fill accent-hover
      border accent-hover hairline
    }
    state hovered when variant = Secondary {
      fill accent-subtle
    }
    state hovered when variant = Ghost {
      fill accent-subtle
      border accent-subtle hairline
    }
    state focus-visible {
      ring accent
    }
    state disabled {
      opacity 50
    }
  }

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