Docs

Styling with CSS

Style the companion with real CSS, including selectors, properties and how they differ from a browser.

The companion screen is drawn natively, but styled with real CSS syntax: structure in layout.json, look in style.css, logic in Lua. Anything a pack doesn't style keeps the built-in look.

Setup

List your stylesheets in manifest.json. This needs formatVersion: 2. They're applied in order, after GBA Duo's built-in styles.

json
{
  "formatVersion": 2,
  "layout": "layout.json",
  "styles": ["style.css"],
  "script": "main.lua"
}

url(...) in a stylesheet resolves relative to that stylesheet. In a widget's inline style it resolves relative to the pack folder. Paths can't leave the pack: ../ outside it, absolute paths and http:// are ignored.

Hooking widgets to CSS

Give widgets an id or class in the layout, then select them. classIf and bindStyle let the data drive the style:

json
{
  "type": "list", "class": "party", "items": "$party",
  "template": {
    "type": "box", "class": "mon", "classIf": { "fainted": "$fainted" },
    "bindStyle": { "--type": "{typeColor}" },
    "children": [
      { "type": "image", "class": "sprite", "src": "$sprite" },
      { "type": "text", "class": "name", "text": "{name}" },
      { "type": "bar", "value": "$hp", "maxKey": "$maxHp", "label": "{hp}/{maxHp}" }
    ]
  }
}
css
:root        { --accent: #e63946; --panel: #1d1f2b; background: #0b0c10; }
@font-face   { font-family: "Pixel"; src: url("fonts/Pixel.ttf"); }

.party       { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 12px; }
.mon         { gap: 4px; padding: 8px; align-items: center;
               background: var(--panel); border: 1px solid var(--type, #2e3245);
               border-radius: 10px; box-shadow: 0 2px 6px rgba(0, 0, 0, .5); }
.mon.fainted { opacity: .4; }
.mon:nth-child(odd) { background: #22263a; }
.sprite      { width: 64px; height: 64px; }
.name        { font-family: "Pixel"; text-transform: uppercase; }
bar          { width: 100%; height: 6px; border-radius: 3px; overflow: hidden; background: #333; }
bar > fill   { background: linear-gradient(90deg, var(--accent), #ffb703); transition: width 300ms ease-out; }
bar > label  { font-size: 9px; }

Element names

Widget types work as element selectors, along with the parts GBA Duo generates:

Element What it is
column, row, box, list, tabs, pages, button, text, bar, image, spacer Widgets
bar > fill, bar > label A bar's fill (width = value/max) and its label
tabs > tab-bar > tab The tab strip and each tab; the selected one matches tab:active
tabs > tab-content The area showing the selected tab
list > empty A list's empty text
button > text.label A button's text

Each part also has a class of the same name, so .fill and .tab work too.

Selectors

Supported Notes
type, *, .class, #id, compounds (.mon.fainted)
Descendant A B and child A > B
:root, :empty, :first-child, :last-child, :nth-child(), :nth-last-child() odd, even, 3, 2n+1, -n+3
:pressed, :active, :disabled :pressed = held down; :active = the selected tab

The cascade, from weakest to strongest: built-in styles, then pack stylesheets (by specificity, then source order), then inline style, then bindStyle. !important is ignored.

Not supported: sibling combinators (+, ~), attribute selectors, pseudo-elements, @media and @import. Those rules are skipped with a warning.

Properties

Units: px (Android dp; unitless numbers are px), %, em, rem, vw, vh, vmin, vmax. Colors: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba(), hsl()/hsla(), named colors, transparent and currentcolor. Custom properties (--x) and var(--x, fallback) work anywhere.

Group Properties
Layout mode display: flex | grid | stack | block | none
Flexbox flex-direction, flex-wrap, flex-flow, justify-content, align-items, align-self, align-content, flex, flex-grow, flex-shrink, flex-basis, order, gap, row-gap, column-gap
Grid grid-template-columns, grid-template-rows (px, %, fr, auto, repeat(n, …), minmax(…, max)), grid-column, grid-row, justify-items, justify-self, place-items, place-self
Positioning position: static | relative | absolute, top, right, bottom, left, inset, z-index
Box width, height, min-*, max-*, aspect-ratio, margin (incl. auto), padding, box-sizing
Background background, background-color, background-image (url(), linear-gradient()), background-size, background-position, background-repeat
Border border, border-{top,right,bottom,left}, border-width/style/color, border-radius; styles solid, dashed, dotted
Effects opacity, box-shadow (first shadow, no inset), transform (translate, scale, rotate), transform-origin, overflow: hidden, visibility
Transitions transition for opacity, color, background-color, transform, width, height
Text color, font-family (+ @font-face), font-size, font-weight, font-style, letter-spacing, line-height, text-align, text-transform, text-shadow, text-decoration, white-space, text-overflow, line-clamp
Images object-fit, image-rendering: pixelated (the default for image)
Bars accent-color colors a bar's fill (or style bar > fill directly)

display: stack is specific to GBA Duo: children are layered on top of each other at the top-left, like Android's FrameLayout. Use place-items to center or stretch them.

Differences from browsers

  • font-size is in sp, so the system text-size setting scales text.
  • box-sizing defaults to border-box.
  • min-width and min-height: auto mean 0 (no min-content sizing). Baseline alignment acts like start. padding doesn't take %.
  • Grid uses row-major auto-placement; there's no grid-template-areas.
  • Not supported (skipped with a warning): @keyframes/animation, filter, radial gradients, multiple backgrounds or shadows.

Debugging

Every skipped rule, unknown property or invalid value is logged:

sh
adb logcat -s Companion PackStyles