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:
context-group Variant { Primary Secondary Ghost }
props { variant : context Variant = Primary disabled : boolean = false label : string = "Get started" }
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 Varianttypes the prop by the group, so its values are the group's contexts. This is the same mechanism as dark mode: a token canalias 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 = Secondaryis hover paint for one variant only. - A state can be driven by a prop.
bind state disabled disabledsays 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.
.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 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 } }