The UI toolkit
Obsidian Glass in code - the screens, panels and widgets in core.ui, how Draw and Theme make the material, the rules every screen keeps, and how this website's stylesheet follows the same contract.
On this page
Every screen in Vantage is drawn by the mod's own toolkit in core.ui, never with vanilla's textured buttons. This
page is for anyone who wants to build a screen that looks like the rest of the mod, in Vantage or in a fork of it:
the pieces, the one shader underneath them, the rules they keep, and how this website follows the same contract.
The binding text is
docs/design/liquid-glass.md in the
repository. Read it before you change anything visual; this page is the guided tour.
The idea
The look is called Obsidian Glass. The canvas is a neutral near-black, every surface on it is a slab of clear glass, and the accent is the light rather than the paint: the glow of a live selection, the tint of a far rim, the aurora behind the window. Hierarchy comes from material and light, never from lines, and no effect may lower the contrast of type.
Three figures belong to the player and between them are most of what "the theme" means. They are options of the Appearance feature and apply live, while the menu is open.
| Figure | Read with | Default | What it moves |
|---|---|---|---|
| Glass opacity | Theme.glassOpacity() | 62 % | How much of every sheet is there. Each material maps it onto its own range; popups barely move. |
| Glass depth | Theme.glassDepth() | 70 % | How thick the glass reads: the bevel, the far-side highlight and its tint. |
| Roundness | Theme.roundness() | 100 % | Every corner, up to the point where controls are capsules. |
Appearance describes the same figures from the player's side.
Where things live
| Package | What is in it |
|---|---|
core.ui | Theme (the palette, the materials, the metrics), Draw (every 2D primitive), UiBootstrap (opening the menu and the HUD editor), ChromaEngine |
core.ui.screen | VScreen, VPopupScreen, the menu (VantageMenuScreen), the widget gallery, the bug report screen |
core.ui.widget | The widgets, the layout containers, PopupLayer, the icons |
core.ui.render | The SDF pipelines and the supersampler |
core.ui.anim | Animation, AnimatedFloat, AnimatedColor, Animator, Easing |
core.ui.notify | Notifier: titles, toasts, sounds and chat lines |
core.ui.config | The menu's feature pages and option rows, the About and Profiles screens |
core.ui.preview | The live previews the menu shows beside some options |
Screens
A full screen extends VScreen. It gets the backdrop (the blurred game or the void, the neutral canvas, one slow
aurora in the accent, a vignette), a transparent root VPanel, a UI scale that keeps a comfortable physical size
from GUI scale 1 to 4 and from 1080p to 4K, input routing in logical pixels, focus and Tab cycling, the popup layer,
tooltips and an entrance animation. Escape closes popups first, then the screen, and onClose() returns to the
parent screen when there is one. A subclass implements build(VPanel root) and may override onResize().
A modal card over another screen extends VPopupScreen and fills the card in buildCard(VColumn body). The parent
keeps rendering underneath, without hover. This is a complete one, shaped like the mod's own About screen:
public final class HelloScreen extends VPopupScreen {
public HelloScreen(@Nullable Screen parent) {
super("Hello", parent);
cardWidth = 260f;
}
@Override
protected void buildCard(VColumn body) {
body.add(new VLabel("A card of popup glass.").muted().shadow(false).wrap(true));
body.add(new VSeparator());
VRow buttons = new VRow(6f, Align.CENTER).justify(Align.END);
buttons.add(new VButton("Close", VButton.Variant.PRIMARY, this::onClose));
body.add(buttons);
}
}new HelloScreen(null).open() shows it. From a chat command, wrap that in Commands.openScreenLater(…): the chat
screen closes itself on the tick the command runs and would take your screen with it.
Commands and keybinds covers that. For the common dialogs you need no class at all:
VPopupScreen.confirm(title, message, confirmLabel, onConfirm) and VPopupScreen.prompt(…) return ready-made ones.
Panels and widgets
VPanel is a sheet of glass with padding and an optional single child. It is described either by a role material
(liveMaterial(…), below), by a sheet you computed (material(Glass)), or by a colour and an elevation from 0
(flat) to 3 (popup). transparent() makes it an invisible grouping container.
| Widget | What it is |
|---|---|
VColumn, VRow | Stack layouts with a gap, a cross-axis alignment and a grow share for leftover space |
VScrollPane | A clipped, eased vertical viewport with a scrollbar that fades in on hover |
VLabel | Text with tones (muted(), ice(), faint()) resolved every frame, wrapping and live text |
VButton | A capsule of control glass in four variants: PRIMARY, SECONDARY, GHOST, DANGER |
VIconButton | An icon-only button; its colours are suppliers, so it follows a preset change |
VToggle, VCheckbox | A sliding switch whose track fills with accent light; a checkbox with an animated mark |
VSlider, VNumberField | A track with a glass knob, step snapping and keyboard nudging; a numeric field with step buttons |
VTextField, VSearchField | Single-line input with selection and clipboard; the same with a search icon and a clear button |
VDropdown | A select box whose list opens on the popup layer, with search once it has more than eight entries |
VColorPicker, VKeybindButton | A swatch that opens a picker with alpha and chroma; a button that captures a key |
VStringList | An editable list of strings, one field per row |
VTabBar | A segmented strip whose highlighted pill slides between tabs |
VCard, VGroupCard | A titled card; one sheet holding many rows separated by inset hairlines |
VBadge, VProgressBar, VSeparator | A chip; a bar of accent light in a well; a divider, optionally captioned |
VTooltip, PopupLayer | Tooltips on popup glass; the layer that draws dropdowns, pickers and dialogs above everything |
Which widget an option gets is decided by its field type; Options and config has that table.
Draw and the material
Draw is the 2D facade every screen and HUD element uses. Coordinates are GUI pixels and floats are allowed
everywhere, so animation stays smooth. Everything is submitted through vanilla's GUI render state, so strata,
layering, scissors and the menu blur behave exactly as they do for vanilla drawing.
The call that makes the look is Draw.glass(g, x, y, w, h, radius, material[, alpha[, gloss]]). A sheet is drawn
in the order light reaches the eye: an ambient shadow; a flat translucent body, never a gradient ramp; a bevel
just inside the edge, where the key light bends in on the lamp side and leaves, tinted in the theme's hue, on the
far side; a specular hairline on the contour; and the pointer's light. Around it are the other primitives:
pointerLight, litPass for ripples and sweeps, focusRing (the only focus ring), bead for capsule knobs,
glow, shadow, dividers, rounded rectangles, arcs and text.
The shader underneath
Steps two to four are one draw on sdf_shape, a shader that evaluates a signed-distance field per fragment and so
anti-aliases rounded rectangles, circles, outlines, soft shadows and arcs analytically at any resolution. Because
vanilla's GUI renderer batches elements and offers no per-element uniforms, every shape parameter travels in vertex
attributes. The pointer's light is a second pass on the same shader, clipped by each sheet's own distance field,
which is why the light can exist only through the glass. The pipeline compiles on first use; if a driver refuses
it, Draw falls back to triangle fans so the interface never disappears, and the UI smoke test fails the run so
that a regression cannot pass quietly. A second shader, sdf_textured, draws textures clipped to a rounded
rectangle, and Supersampler renders Vantage's own screens at a multiple of the window's resolution, which helps
text and the fallback geometry and nothing else.
Theme
Nothing in the palette is a literal. Theme derives every tone from one (canvas, accent) pair: a preset, the
player's own accent, or a live accent override a feature installs. The presets are GLACIER (the blue default),
OBSIDIAN, EMBER, VERDANT, ROSE and GRAPHITE, all on near-neutral canvases.
Read Theme at draw time, never into a field. Changing the accent or a figure rebuilds the palette at once, and a
value captured at construction keeps the old one until the screen is rebuilt.
Five materials
Pick a material by role, not by look:
| Material | Depth | Role | Examples |
|---|---|---|---|
Theme.glassChrome() | 0.55 | The frame of a screen | The menu window, the wizard frame |
Theme.glassCard() | 0.7 | Content sheets sitting on chrome | Option groups, feature rows, the sidebar pane |
Theme.glassControl() | 1.0 | Anything clickable | Buttons, dropdowns, swatches, keybind buttons |
Theme.glassPopup() | 0.8 | Must be readable over anything | Menus, tooltips, tags over the HUD canvas |
Theme.glassWell() | −0.85 | Cut into the surface | Text fields, slider and progress tracks, tab strips |
A negative depth is a well: the same slab recessed, lit on its far lip, so nothing about it is drawn by hand.
Theme.glassFrom(colour, elevation) turns a flat colour into a sheet, Theme.glassWellLit(t) is a well filling with
accent light (toggles and checkboxes), .tinted(colour, amount) warms a sheet toward a colour, and .withDepth(d)
changes how thick it reads.
Glass over something sharp is popup glass. Behind a menu the world is blurred, and chrome at the player's
opacity is easy to read on. The HUD editor's bars float over the live HUD with no blur, so they wear
glassPopup() whatever the opacity figure says.
Gloss: how much light a sheet catches
The caller chooses only one number about the pointer's light, gloss:
| Sheet | Gloss |
|---|---|
| Controls | 1f, plus 0.5f * hover while the cursor is on them |
| Content cards | 0.5f to 0.6f |
| The frame of a screen | 0.35f to 0.4f: a full-strength lobe on a sheet that large reads as a torch |
| Wells and tracks | 0.4f to 0.5f |
| Anything disabled | 0f |
liveMaterial, not a snapshot
A Theme.Glass is a snapshot of the palette at the moment you asked for it. A window that is just "the chrome"
must ask for it live, or it keeps its old opacity while the player drags the opacity slider on the very page it is
framing:
VPanel window = new VPanel().liveMaterial(Theme::glassChrome); // follows every change, every frame
// A sheet you computed is the one case for material(…): here, the selection lens.
VPanel lens = new VPanel().material(Theme.glassControl().tinted(Theme.accent(), 0.5f).withDepth(1f));The same holds for tones: VLabel.muted(), ice() and faint() resolve per frame, and colours handed to a widget
as IntSuppliers are read when it draws.
Shape: radiusControl and the fit rule
There is one radius scale, so a control inside a panel looks nested rather than repeated. Each step slides with the roundness figure from its precise value to its bubbly one:
| Step | Precise (roundness 0) | Bubbly (roundness 1) |
|---|---|---|
radiusTiny() | 2 | 5 |
radiusSmall() | 4 | 9 |
radius() | 6 | 13 |
radiusLarge() | 9 | 19 |
radiusXLarge() | 12 | 26 |
Containers take the scale: radiusLarge() for cards, radiusXLarge() for a window, stepping down with each level
of nesting so that nesting reads as nesting. Controls never pick a step. They ask Theme.radiusControl(height), which is min(h / 2, 3 + (h / 2 − 3) × roundness):
a true capsule at full roundness, a 3 px corner at none. Buttons, icon buttons, fields, dropdowns, tab strips,
badges and tracks all ask there, so they stay one family at any setting. Knobs (Draw.bead) are capsules too.
What sits inside a round end has to fit it. A rectangle that merely fits a control's height crosses the curve: 3 px in from the end of a 22 px capsule, the curve has already taken 3.5 px. So anything drawn inside a control is concentric with it, with the same gap all the way round (4 px for a 22 px control) and the control's corner scaled to the inner size:
@Override
protected void draw(GuiGraphicsExtractor g, double mouseX, double mouseY, float delta) {
Draw.glass(g, x, y, w, h, Theme.radiusControl(h), Theme.glassControl(), 1f, 1f);
float gap = 4f; // the same on every side
float inner = h - 2 * gap;
Draw.roundedRect(g, x + gap, y + gap, inner, inner, Theme.radiusControl(inner), swatch);
}Square content that cannot be rounded, such as a checkerboard, is drawn only where it falls wholly inside the shape. Feedback that covers part of a control is the whole control's shape under a scissor, never a rectangle of its own. A selection rail sits 2 px inside its row, not on the edge.
The rules
Each of these is in the contract, with its reason:
- No outlines as structure. Depth is shadow, tint, bevel and rim. The only rings are
Draw.focusRingand the handles on the HUD editor's canvas. - No hard-coded chrome colours. Backgrounds, borders, dividers, text tones and accents come from
Theme. Data colours are different: a health bar is red and a rarity is purple, because those are meaning, not chrome. - No tones or radii captured at construction. Read them when you draw.
- No flat swatches for selection. Selected is a lens: accent-tinted control glass at full depth with a soft
Draw.glow. Where a list has one selection, one lens slides between rows, as in the menu's sidebar. - No text shadows on glass in menus. HUD elements keep the player's own text-shadow option.
- No hard-coded durations. There are three, 220, 340 and 520 ms (
Theme.animationMs(),animationSlowMs(),animationLanguidMs()), and all collapse to 1 ms when the player turns on Reduce motion.Easing.OUT_SPRINGis the house spring for things arriving; things being put away never bounce. - Glow is emission. It belongs on the few things genuinely emitting, and the player scales the rest.
- Light only through glass. Never paint a halo or a spot under the pointer outside a sheet.
- The backdrop is the game, the canvas, one aurora and a vignette. Nothing else.
Icons
Icons are the Lucide set, vendored as geometry in IconShapes.java rather than as images, so they tint with the
theme and stay sharp at every GUI scale. python tools/icons.py regenerates that file from Lucide's repository. It
is deliberately not run by CI, because its --check downloads from Lucide's main branch and an upstream commit
would otherwise turn the pipeline red. Lucide's ISC licence ships in the jar beside Inter's.
Seeing your work
- The widget gallery shows every widget in every state. Open it with
/vt dev gallery, or with the Widget gallery button in the menu. - The UI smoke test walks every Vantage screen and screenshots it. The
menu-litandgallery-litframes have the cursor parked over the content, so they show the pointer's light;menu-embershows a second preset. Testing covers running it and reading its verdict. - Before you call a visual change done, the contract asks for
./gradlew compileJavaon both Minecraft targets,./gradlew test, then the smoke test, and a look at the screenshots rather than the exit code.
How this website follows the same contract
The page you are reading is drawn to the same rules, in CSS. One conversion governs every number: 1 mod logical
pixel is 2 CSS pixels, held in a single custom property, --u: 2px.
| In the mod | On this site | Value on the site |
|---|---|---|
Theme.controlHeight(), 22 | --h-control | 44 px |
Theme.rowHeight(), 26 | --h-row | 52 px |
The 4 px grid, space1() to space6() | --s1 to --s6 | An 8 px grid, 8 to 64 |
radiusTiny() to radiusXLarge() | --r-tiny, --r-small, --r, --r-lg, --r-xl | The same precise-to-bubbly formula, times --u |
radiusControl(h) | --r-ctl, from each control's own height | A capsule at full roundness |
| The three durations | --dur, --dur-slow, --dur-languid | 220, 340 and 520 ms, collapsing under reduced motion |
| The six presets | [data-theme] blocks, Glacier by default | The same (canvas, accent) pairs |
The five materials are five CSS roles, .glass--chrome, .glass--card, .glass--ctl, .glass--popup and
.glass--well, plus .glass--accent for the lit sheet of a primary button or a tab pill. Their depths and gloss
values are the mod's numbers, and their body opacity is the mod's formula: each role maps the glass-opacity figure
onto the same range it has in the game. Every sheet carries three decorative layers, a bevel, a hairline and a
lamp, which are the material's steps three to five. The lamp follows the pointer and, as in the mod, lights only
through glass.
Two things differ, because a browser is not a game. In the game the world behind a menu is blurred once, as part of the backdrop; on the web, chrome and popup sheets carry the blur themselves and the other roles carry none, since a blur over the smooth backdrop costs a compositor layer and shows nothing. And anything that stays put while the page scrolls under it is popup glass, which is the web's version of "glass over something sharp".
Every colour on the site is a custom property, so a preset change restyles a page without re-rendering it. That is
the same reason the mod reads Theme at draw time.
Related
- Architecture: where
core.uisits among the other packages. - Options and config: which widget an option gets.
- HUD elements: drawing on the HUD with the same materials.
- Testing: the widget gallery's big brother, the UI smoke test.