1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/* Button
No border, no ornament — hierarchy is ink density, not an outline. A
STAMP (primary, dark, danger) is a solid block of colour; a QUIET action
(default, ghost) carries no fill at rest and picks up a flat tint of the
stock on hover, the same gesture as a CardRow. Variants: default, primary,
dark, ghost, danger. Sizes: sm, md (default), lg — padding and font step
only, weight and radius stay put.
The default used to pad itself with --gl-space-xs/-s, which are the rungs
a layout reaches for between two controls, not inside one: on a wide
viewport they grew the default button taller than the large one. Every
size now sits one rung further down the same scale — see the block at
the bottom.
The contract is the element itself: a bare <button> is styled directly, no
class required. An <a> that acts like a button (navigates rather than
changes something — see the Button page's "Links that act like buttons")
opts in with a single `.btn` class, since a bare `a` selector would catch
every ordinary prose link on the site. `:not(.icon-button)` keeps this
block from leaking onto IconButton, which shares the <button> tag but is
an entirely different shape. */
button:not(.icon-button),
a.btn {
--_bg: transparent;
--_fg: var(--gl-color-text-primary);
--_pad-y: var(--gl-space-2xs);
--_pad-x: var(--gl-space-xs);
--_font: var(--gl-step--1);
/* The flex gap already separates the icon from the label. */
--gl-icon-start: var(--gl-icon);
--gl-icon-style-spacing: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--gl-space-2xs);
padding: var(--_pad-y) var(--_pad-x);
border: none;
border-radius: var(--gl-radius-md);
background: var(--_bg);
color: var(--_fg);
font-family: var(--gl-font-mono);
font-size: var(--_font);
font-weight: 500;
letter-spacing: 0.01em;
line-height: 1.15;
text-align: center;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: background-color var(--gl-dur-fast) var(--gl-ease),
color var(--gl-dur-fast) var(--gl-ease),
filter var(--gl-dur-fast) var(--gl-ease);
}
/* Quiet variants (default, ghost) carry no fill at rest; hovering picks up
a flat tint of the stock instead of darkening a border that isn't there.
Tabs are <button>s too but own their hover (tabs.css): this rule's
specificity would otherwise paint the selected segment the colour of its
own track until the pointer left it. */
:is(button:not(.icon-button), a.btn):not([role="tab"]):not([data-variant="primary"]):not([data-variant="dark"]):not([data-variant="danger"]):hover:not(:disabled):not([aria-disabled="true"]) {
background: var(--gl-color-surface-fill);
color: var(--gl-color-text-primary);
}
/* Stamp variants (primary, dark, danger) are already ink — hovering just
darkens the fill in place. */
:is(button:not(.icon-button), a.btn)[data-variant="primary"]:hover:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="dark"]:hover:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="danger"]:hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.96); }
:is(button:not(.icon-button), a.btn)[data-variant="primary"]:active:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="dark"]:active:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="danger"]:active:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.92); }
button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[aria-disabled="true"] {
opacity: 0.5;
cursor: default;
}
:is(button:not(.icon-button), a.btn)[data-variant="primary"] {
--_bg: var(--gl-color-accent);
--_fg: var(--gl-color-accent-fg);
}
/* Reserved for the single most-committing action in a view — a final Save,
never a whole row of them. */
:is(button:not(.icon-button), a.btn)[data-variant="dark"] {
--_bg: var(--gl-color-text-primary);
--_fg: var(--gl-color-surface-page);
}
/* Same quiet block as default, one step further back: text-secondary at
rest instead of text-primary. */
:is(button:not(.icon-button), a.btn)[data-variant="ghost"] {
--_fg: var(--gl-color-text-secondary);
}
/* The source hardcodes this label white rather than deriving it — danger
is not "on a filled accent surface", so --gl-color-accent-fg is reused
here only because its resolved value is the same literal white, not
because the two roles are the same thing. */
:is(button:not(.icon-button), a.btn)[data-variant="danger"] {
--_bg: var(--gl-color-danger);
--_fg: var(--gl-color-accent-fg);
}
/* Each size is one step of the space scale wide and, at the ends, one step
of the type scale. Medium and large share a rung vertically and part on
type size, which is what keeps them in order at every viewport width: the
scale is fluid, so two sizes that differed only by which fluid rung they
padded with could — and did — cross over on a wide screen. */
:is(button:not(.icon-button), a.btn)[data-size="sm"] { --_pad-y: var(--gl-space-3xs); --_pad-x: var(--gl-space-2xs); --_font: 0.8125rem; }
:is(button:not(.icon-button), a.btn)[data-size="lg"] { --_pad-x: var(--gl-space-s); --_font: var(--gl-step-0); }
:is(button:not(.icon-button), a.btn)[data-block] { width: 100%; }
/* Button group — [role="group"] is already the correct ARIA role for a
toolbar of related buttons, so it doubles as the styling hook for free.
A hairline divider stands in for the border the buttons themselves no
longer carry — without one, two adjacent quiet buttons would read as a
single wide button. */
[role="group"] {
display: inline-flex;
isolation: isolate;
}
[role="group"] > button { border-radius: 0; }
[role="group"] > button:first-child { border-start-start-radius: var(--gl-radius-md); border-end-start-radius: var(--gl-radius-md); }
[role="group"] > button:last-child { border-start-end-radius: var(--gl-radius-md); border-end-end-radius: var(--gl-radius-md); }
[role="group"] > button + button { border-inline-start: 1px solid var(--gl-color-border-hairline); }
[role="group"] > button:hover,
[role="group"] > button:focus-visible { z-index: 1; }
/* IconButton — a square icon-only button. Always needs an aria-label; it
becomes both the accessible name and the native tooltip. There is no
attribute that distinguishes "a button that is only an icon" from a plain
Button, so this is the one sanctioned class in this file.
The box is not a number of its own: it is written the way Button's height
arrives — two rungs of padding around one 1.15 line of the label type it
would carry if it had a label. So the two are the same height at every
viewport width, and stay that way when the scale is retuned, instead of a
fixed square drifting away from a fluid button. */
.icon-button {
--_pad: var(--gl-space-2xs);
--_line: var(--gl-step--1);
--_size: calc(2 * var(--_pad) + 1.15 * var(--_line));
--_bg: transparent;
--_fg: var(--gl-color-text-secondary);
/* No spacing, so stray whitespace in the markup can't push the icon
off-center. */
--gl-icon-start: var(--gl-icon);
--gl-icon-style-spacing: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--_size);
height: var(--_size);
padding: 0;
flex: none;
border: none;
border-radius: var(--gl-radius-md);
background: var(--_bg);
color: var(--_fg);
font-size: var(--_line);
cursor: pointer;
transition: background-color var(--gl-dur-fast) var(--gl-ease),
color var(--gl-dur-fast) var(--gl-ease);
}
.icon-button:hover:not(:disabled) { background: var(--gl-color-surface-fill); }
.icon-button:disabled { opacity: 0.5; cursor: default; }
/* Same two overrides Button's own sm and lg make, so the pairs stay level. */
.icon-button[data-size="sm"] { --_pad: var(--gl-space-3xs); --_line: 0.8125rem; }
.icon-button[data-size="lg"] { --_line: var(--gl-step-0); }
/* No border on either variant — "outline" keeps its name for API
compatibility, but reads as a raised fill instead of a bordered box. */
.icon-button[data-variant="outline"] { --_bg: var(--gl-color-surface-fill); --_fg: var(--gl-color-text-primary); }
.icon-button[data-variant="accent"] { --_bg: var(--gl-color-accent-soft); --_fg: var(--gl-color-accent-ink); }