All pages

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.

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.

FigureRead withDefaultWhat it moves
Glass opacityTheme.glassOpacity()62 %How much of every sheet is there. Each material maps it onto its own range; popups barely move.
Glass depthTheme.glassDepth()70 %How thick the glass reads: the bevel, the far-side highlight and its tint.
RoundnessTheme.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

PackageWhat is in it
core.uiTheme (the palette, the materials, the metrics), Draw (every 2D primitive), UiBootstrap (opening the menu and the HUD editor), ChromaEngine
core.ui.screenVScreen, VPopupScreen, the menu (VantageMenuScreen), the widget gallery, the bug report screen
core.ui.widgetThe widgets, the layout containers, PopupLayer, the icons
core.ui.renderThe SDF pipelines and the supersampler
core.ui.animAnimation, AnimatedFloat, AnimatedColor, Animator, Easing
core.ui.notifyNotifier: titles, toasts, sounds and chat lines
core.ui.configThe menu's feature pages and option rows, the About and Profiles screens
core.ui.previewThe 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.

WidgetWhat it is
VColumn, VRowStack layouts with a gap, a cross-axis alignment and a grow share for leftover space
VScrollPaneA clipped, eased vertical viewport with a scrollbar that fades in on hover
VLabelText with tones (muted(), ice(), faint()) resolved every frame, wrapping and live text
VButtonA capsule of control glass in four variants: PRIMARY, SECONDARY, GHOST, DANGER
VIconButtonAn icon-only button; its colours are suppliers, so it follows a preset change
VToggle, VCheckboxA sliding switch whose track fills with accent light; a checkbox with an animated mark
VSlider, VNumberFieldA track with a glass knob, step snapping and keyboard nudging; a numeric field with step buttons
VTextField, VSearchFieldSingle-line input with selection and clipboard; the same with a search icon and a clear button
VDropdownA select box whose list opens on the popup layer, with search once it has more than eight entries
VColorPicker, VKeybindButtonA swatch that opens a picker with alpha and chroma; a button that captures a key
VStringListAn editable list of strings, one field per row
VTabBarA segmented strip whose highlighted pill slides between tabs
VCard, VGroupCardA titled card; one sheet holding many rows separated by inset hairlines
VBadge, VProgressBar, VSeparatorA chip; a bar of accent light in a well; a divider, optionally captioned
VTooltip, PopupLayerTooltips 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:

MaterialDepthRoleExamples
Theme.glassChrome()0.55The frame of a screenThe menu window, the wizard frame
Theme.glassCard()0.7Content sheets sitting on chromeOption groups, feature rows, the sidebar pane
Theme.glassControl()1.0Anything clickableButtons, dropdowns, swatches, keybind buttons
Theme.glassPopup()0.8Must be readable over anythingMenus, tooltips, tags over the HUD canvas
Theme.glassWell()−0.85Cut into the surfaceText 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:

SheetGloss
Controls1f, plus 0.5f * hover while the cursor is on them
Content cards0.5f to 0.6f
The frame of a screen0.35f to 0.4f: a full-strength lobe on a sheet that large reads as a torch
Wells and tracks0.4f to 0.5f
Anything disabled0f

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:

StepPrecise (roundness 0)Bubbly (roundness 1)
radiusTiny()25
radiusSmall()49
radius()613
radiusLarge()919
radiusXLarge()1226

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.focusRing and 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_SPRING is 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-lit and gallery-lit frames have the cursor parked over the content, so they show the pointer's light; menu-ember shows a second preset. Testing covers running it and reading its verdict.
  • Before you call a visual change done, the contract asks for ./gradlew compileJava on 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 modOn this siteValue on the site
Theme.controlHeight(), 22--h-control44 px
Theme.rowHeight(), 26--h-row52 px
The 4 px grid, space1() to space6()--s1 to --s6An 8 px grid, 8 to 64
radiusTiny() to radiusXLarge()--r-tiny, --r-small, --r, --r-lg, --r-xlThe same precise-to-bubbly formula, times --u
radiusControl(h)--r-ctl, from each control's own heightA capsule at full roundness
The three durations--dur, --dur-slow, --dur-languid220, 340 and 520 ms, collapsing under reduced motion
The six presets[data-theme] blocks, Glacier by defaultThe 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.