Gavia UIv1.0.0

Design system

Rules for text, colors, spacing and component states. Tokens and examples for five themes.
60 components447 tokens113 icons5 themes

01 / Foundations

Actions, states and groups

Highlight one primary action. Use text or an icon alongside color to convey state. Group related elements with spacing and shared backgrounds.

Labels and states

Name the action directly. Show loading, errors and the result.

Sizes and behavior

Use shared sizes and spacing. Similar elements should behave consistently.

Accessibility

Keyboard support, visible focus, field labels and sufficient contrast.

Gavia

Light theme with Gavia Sans.

Gavia Dark

Dark theme with a teal accent and Gavia Sans.

Classic

Light theme with a blue accent.

Classic Dark

Dark theme with status colors.

Newspaper

Light theme with a warm paper background.

Surface

Cards and sections / elevation-surface

Floating layer

Menus and panels / elevation-floating

02 / Typography

Text roles

Size follows the text role. Semantic h1–h6 elements preserve document structure; the class defines the appearance. Newspaper uses its own fonts through the same tokens.

display

40px / 48px · 700

Display

Main introductory heading; one per screen.

title

28px / 36px · 700

Page title

Page and large dialog title.

heading

20px / 28px · 650

Section heading

Page section and independent block.

subheading

16px / 24px · 600

Block heading

Card, field group or small dialog.

body

14px / 20px · 400

Body text

Descriptions, instructions and form text.

small

12px / 18px · 400

Small text

Captions and supporting text.

label

13px / 20px · 500

Label

Short action or field label.

code

12px / 18px · 400

Code and data

Tokens, identifiers and technical values.

Use body for main content and small + text-muted for supporting text. Keep long lines around 60–75 characters. --wl-text-3 is reserved for decorative details.

03 / Grid and spacing

Spacing scale

Small groups: 8 px; form fields: 16 px; cards: 24 px; sections: 48–64 px. Compact density suits screens with a lot of data.

none0px
2xs2px
xs4px
sm8px
md12px
lg16px
xl24px
2xl32px
3xl48px
4xl64px

Responsive grid

Card A
Card B

Cards automatically switch to one column. Container: 1240px.

HTML · layout primitives
<section class="wl-container">
  <div class="wl-stack" data-space="xl">
    <h1 class="wl-text-title">Page title</h1>
    <div class="wl-grid" data-space="lg">
      <article class="wl-surface">Content</article>
    </div>
  </div>
</section>
sm: 640 pxmd: 900 pxlg: 1200 px

Import CSS explicitly: styles/primitives.css. Keep a logical reading order on narrow screens, wrap actions and limit horizontal scrolling to tables.

04 / Tokens

Value catalog

Foundation → semantic → component. The catalog, CSS and types are generated from tokens/source.json. Values below use the theme gavia.

CSS · customizing tokens
.page {
  color: var(--wl-text);
  background: var(--wl-bg);
  gap: var(--wl-space-lg);
}
/* Customize a specific component */
.page-actions { --wl-btn-height: var(--wl-control-height-lg); }
Layer
Category

Tokens found: 171 / 447

Token / roleReferenceValue
--wl-type-text-rendering

Text rendering mode for the selected theme.

semantic
var(--wl-font-text-rendering)geometricPrecision
--wl-bg

Primary page and content surface.

semantic
var(--wl-palette-bg)#faf9f6
--wl-bg-raised

Light surface for cards and local panels above the page background.

semantic
var(--wl-palette-bg-raised)#fefdfb
--wl-bg-soft

Grouped blocks and secondary surfaces.

semantic
var(--wl-palette-bg-soft)#eef2f3
--wl-bg-hover

Interactive element surface on hover.

semantic
var(--wl-palette-bg-hover)#e5ecef
--wl-border

Subtle surface dividers.

semantic
var(--wl-palette-border)#d5dfe3
--wl-border-2

Stronger group border.

semantic
var(--wl-palette-border-2)#b8c7ce
--wl-text

Primary readable text.

semantic
var(--wl-palette-text)#0f1a23
--wl-text-accent

Accent text for links, quiet actions and selected items on standard and soft surfaces.

semantic
var(--wl-palette-text-accent)#326179
--wl-text-accent-hover

Accent text on hover for links and quiet actions.

semantic
var(--wl-palette-accent-hover)#326179
--wl-text-2

Secondary readable text and labels.

semantic
var(--wl-palette-text-2)#5b6470
--wl-text-3

Muted decorative metadata; not for required instructions.

semantic
var(--wl-palette-text-3)#75818c
--wl-accent

Accent for selection, links and primary actions.

semantic
var(--wl-palette-accent)#3c7490
--wl-accent-hover

Accent on hover.

semantic
var(--wl-palette-accent-hover)#326179
--wl-accent-soft

Role color accent-soft.

semantic
var(--wl-palette-accent-soft)#dfe8eb
--wl-accent-border

Role color accent-border.

semantic
var(--wl-palette-accent-border)#b8cbd3
--wl-accent-soft-hover

Role color accent-soft-hover.

semantic
var(--wl-palette-accent-soft-hover)#d3dfe4
--wl-accent-border-hover

Role color accent-border-hover.

semantic
var(--wl-palette-accent-border-hover)#9eb7c2
--wl-on-accent

Legacy color on the accent surface.

semantic
var(--wl-palette-on-accent)#ffffff
--wl-success

Successful result signal.

semantic
var(--wl-palette-success)#2e9e68
--wl-success-soft

Role color success-soft.

semantic
var(--wl-palette-success-soft)#eaf6f0
--wl-success-border

Role color success-border.

semantic
var(--wl-palette-success-border)#cdeadb
--wl-warn

Warning signal.

semantic
var(--wl-palette-warn)#bf8615
--wl-warn-soft

Role color warn-soft.

semantic
var(--wl-palette-warn-soft)#fbf4e2
--wl-warn-border

Role color warn-border.

semantic
var(--wl-palette-warn-border)#f0dfae
--wl-danger

Destructive action or error signal.

semantic
var(--wl-palette-danger)#ab4448
--wl-danger-hover

Role color danger-hover.

semantic
var(--wl-palette-danger-hover)#963c3f
--wl-danger-soft

Role color danger-soft.

semantic
var(--wl-palette-danger-soft)#f7eeed
--wl-danger-soft-hover

Role color danger-soft-hover.

semantic
var(--wl-palette-danger-soft-hover)#f3e5e4
--wl-danger-border

Role color danger-border.

semantic
var(--wl-palette-danger-border)#e5c6c5
--wl-info-text

Role color info-text.

semantic
var(--wl-palette-info-text)#1e3f8f
--wl-ok-text

Role color ok-text.

semantic
var(--wl-palette-ok-text)#1c6b47
--wl-warn-text

Role color warn-text.

semantic
var(--wl-palette-warn-text)#7d5a0e
--wl-err-text

Role color err-text.

semantic
var(--wl-palette-err-text)#ab4448
--wl-mask-bg

Modal backdrop.

semantic
var(--wl-palette-mask-bg)rgba(15, 26, 35, 0.28)
--wl-avatar-bg

Role color avatar-bg.

semantic
var(--wl-palette-avatar-bg)#253540
--wl-tooltip-bg

Tooltip surface.

semantic
var(--wl-palette-tooltip-bg)#0f1a23
--wl-tooltip-text

Tooltip text.

semantic
var(--wl-palette-tooltip-text)#ffffff
--wl-control-height-md

Role btn-height.

semantic
var(--wl-dimension-btn-height)40px
--wl-control-height-sm

Role btn-height-sm.

semantic
var(--wl-dimension-btn-height-sm)32px
--wl-control-height-lg

Role btn-height-lg.

semantic
var(--wl-dimension-btn-height-lg)48px
--wl-control-height-xs

Role btn-height-xs.

semantic
var(--wl-dimension-btn-height-xs)26px
--wl-corner-control

Role btn-radius.

semantic
var(--wl-radius)8px
--wl-corner-control-sm

Role btn-radius-sm.

semantic
var(--wl-radius-sm)6px
--wl-control-padding-inline

Role btn-pad-x.

semantic
var(--wl-dimension-btn-pad-x)14px
--wl-type-control-size

Role btn-font-size.

semantic
var(--wl-dimension-btn-font-size)13.5px
--wl-corner-input

Role input-radius.

semantic
var(--wl-dimension-input-radius)6px
--wl-focus-shadow

Role focus-ring.

semantic
0 0 0 3px var(--wl-accent-soft)0 0 0 3px #dfe8eb
--wl-focus-invalid-shadow

Role focus-ring-danger.

semantic
0 0 0 3px var(--wl-danger-soft)0 0 0 3px #f7eeed
--wl-metric-check-size

Role check-size.

semantic
var(--wl-dimension-check-size)18px
--wl-metric-switch-width

Role switch-width.

semantic
var(--wl-dimension-switch-width)36px
--wl-metric-switch-height

Role switch-height.

semantic
var(--wl-dimension-switch-height)22px
--wl-metric-tag-height

Role tag-height.

semantic
var(--wl-dimension-tag-height)20px
--wl-metric-chip-height

Role chip-height.

semantic
var(--wl-dimension-chip-height)30px
--wl-metric-badge-height

Role badge-height.

semantic
var(--wl-dimension-badge-height)18px
--wl-corner-surface

Role card-radius.

semantic
var(--wl-radius)8px
--wl-corner-overlay

Role dialog-radius.

semantic
var(--wl-radius-xl)12px
--wl-layout-drawer-width

Role drawer-width.

semantic
var(--wl-dimension-drawer-width)420px
--wl-layer-mask

Role mask-z.

semantic
var(--wl-dimension-mask-z)70
--wl-layer-popover

Role overlay-z.

semantic
var(--wl-dimension-overlay-z)80
--wl-metric-iconbtn-size-sm

Role iconbtn-size-sm.

semantic
var(--wl-dimension-iconbtn-size-sm)30px
--wl-metric-sidebar-width

Role sidebar-width.

semantic
var(--wl-dimension-sidebar-width)68px
--wl-metric-sidebar-expanded-width

Role sidebar-expanded-width.

semantic
var(--wl-dimension-sidebar-expanded-width)228px
--wl-layer-navigation

Role sidebar-z.

semantic
var(--wl-dimension-sidebar-z)40
--wl-layer-navigation-modal

Role sidebar-mobile-z.

semantic
var(--wl-dimension-sidebar-mobile-z)90
--wl-metric-segmented-shadow

Role segmented-shadow.

semantic
0 1px 2px var(--wl-ink-a08)0 1px 2px rgba(20, 22, 28, 0.08)
--wl-layout-menu-width

Role menu-width.

semantic
var(--wl-dimension-menu-width)230px
--wl-layout-popover-width

Role popover-width.

semantic
var(--wl-dimension-popover-width)280px
--wl-layout-command-width

Role command-width.

semantic
var(--wl-dimension-command-width)580px
--wl-metric-command-max-height

Role command-max-height.

semantic
var(--wl-dimension-command-max-height)420px
--wl-metric-command-input-height

Role command-input-height.

semantic
var(--wl-dimension-command-input-height)52px
--wl-layer-command

Role command-z.

semantic
var(--wl-dimension-command-z)90
--wl-metric-page-header-gap

Role page-header-gap.

semantic
var(--wl-dimension-page-header-gap)16px
--wl-metric-filter-bar-gap

Role filter-bar-gap.

semantic
var(--wl-dimension-filter-bar-gap)10px
--wl-metric-filter-drawer-width

Role filter-drawer-width.

semantic
var(--wl-dimension-filter-drawer-width)380px
--wl-layer-filter

Role filter-z.

semantic
var(--wl-dimension-filter-z)90
--wl-layer-toast

Role toast-z.

semantic
var(--wl-dimension-toast-z)100
--wl-status-info-icon

Role toast-icon-info.

semantic
var(--wl-blue-300)#9cbdf0
--wl-status-success-icon

Role toast-icon-ok.

semantic
var(--wl-green-400)#4cc38a
--wl-status-warning-icon

Role toast-icon-warn.

semantic
var(--wl-amber-400)#e0a93e
--wl-status-danger-icon

Role toast-icon-err.

semantic
var(--wl-palette-danger)#ab4448
--wl-metric-stepper-input-width

Role stepper-input-width.

semantic
var(--wl-dimension-stepper-input-width)76px
--wl-metric-stepper-btn-width

Role stepper-btn-width.

semantic
var(--wl-dimension-stepper-btn-width)36px
--wl-metric-slider-height

Role slider-height.

semantic
var(--wl-dimension-slider-height)6px
--wl-metric-slider-thumb

Role slider-thumb.

semantic
var(--wl-dimension-slider-thumb)18px
--wl-metric-step-num

Role step-num.

semantic
var(--wl-dimension-step-num)24px
--wl-metric-table-cell-pad-y

Role table-cell-pad-y.

semantic
var(--wl-dimension-table-cell-pad-y)11px
--wl-metric-table-cell-pad-x

Role table-cell-pad-x.

semantic
var(--wl-dimension-table-cell-pad-x)12px
--wl-metric-table-head-pad-y

Role table-head-pad-y.

semantic
var(--wl-dimension-table-head-pad-y)8px
--wl-metric-stat-bar-height

Role stat-bar-height.

semantic
var(--wl-dimension-stat-bar-height)3px
--wl-metric-colorpick-sw

Role colorpick-sw.

semantic
var(--wl-dimension-colorpick-sw)26px
--wl-metric-colorpick-sw-sm

Role colorpick-sw-sm.

semantic
var(--wl-dimension-colorpick-sw-sm)20px
--wl-metric-colorpick-gap

Role colorpick-gap.

semantic
var(--wl-dimension-colorpick-gap)8px
--wl-metric-cal-gap

Role cal-gap.

semantic
var(--wl-dimension-cal-gap)5px
--wl-metric-cal-day-min-h

Role cal-day-min-h.

semantic
var(--wl-dimension-cal-day-min-h)60px
--wl-metric-cal-ev-size

Role cal-ev-size.

semantic
var(--wl-dimension-cal-ev-size)10.5px
--wl-metric-dp-day-size

Role dp-day-size.

semantic
var(--wl-dimension-dp-day-size)30px
--wl-metric-dp-panel-pad

Role dp-panel-pad.

semantic
var(--wl-dimension-dp-panel-pad)12px
--wl-metric-upload-drop-pad-y

Role upload-drop-pad-y.

semantic
var(--wl-dimension-upload-drop-pad-y)26px
--wl-metric-upload-drop-pad-x

Role upload-drop-pad-x.

semantic
var(--wl-dimension-upload-drop-pad-x)20px
--wl-space-none

Spacing none: 0 px.

semantic
var(--wl-space-0)0px
--wl-space-2xs

Spacing 2xs: 2 px.

semantic
var(--wl-space-2)2px
--wl-space-xs

Spacing xs: 4 px.

semantic
var(--wl-space-4)4px
--wl-space-sm

Spacing sm: 8 px.

semantic
var(--wl-space-8)8px
--wl-space-md

Spacing md: 12 px.

semantic
var(--wl-space-12)12px
--wl-space-lg

Spacing lg: 16 px.

semantic
var(--wl-space-16)16px
--wl-space-xl

Spacing xl: 24 px.

semantic
var(--wl-space-24)24px
--wl-space-2xl

Spacing 2xl: 32 px.

semantic
var(--wl-space-32)32px
--wl-space-3xl

Spacing 3xl: 48 px.

semantic
var(--wl-space-48)48px
--wl-space-4xl

Spacing 4xl: 64 px.

semantic
var(--wl-space-64)64px
--wl-type-display-size

Main introductory heading; one per screen.

semantic
var(--wl-font-display-size)40px
--wl-type-display-line-height

Main introductory heading; one per screen.

semantic
var(--wl-font-display-line-height)48px
--wl-type-display-weight

Main introductory heading; one per screen.

semantic
var(--wl-font-display-weight)700
--wl-type-display-family

Main introductory heading; one per screen.

semantic
var(--wl-font-display-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-title-size

Page and large dialog title.

semantic
var(--wl-font-title-size)28px
--wl-type-title-line-height

Page and large dialog title.

semantic
var(--wl-font-title-line-height)36px
--wl-type-title-weight

Page and large dialog title.

semantic
var(--wl-font-title-weight)700
--wl-type-title-family

Page and large dialog title.

semantic
var(--wl-font-title-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-heading-size

Page section and independent block.

semantic
var(--wl-font-heading-size)20px
--wl-type-heading-line-height

Page section and independent block.

semantic
var(--wl-font-heading-line-height)28px
--wl-type-heading-weight

Page section and independent block.

semantic
var(--wl-font-heading-weight)650
--wl-type-heading-family

Page section and independent block.

semantic
var(--wl-font-heading-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-subheading-size

Card, field group or small dialog.

semantic
var(--wl-font-subheading-size)16px
--wl-type-subheading-line-height

Card, field group or small dialog.

semantic
var(--wl-font-subheading-line-height)24px
--wl-type-subheading-weight

Card, field group or small dialog.

semantic
var(--wl-font-subheading-weight)600
--wl-type-subheading-family

Card, field group or small dialog.

semantic
var(--wl-font-subheading-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-body-size

Descriptions, instructions and form text.

semantic
var(--wl-font-body-size)14px
--wl-type-body-line-height

Descriptions, instructions and form text.

semantic
var(--wl-font-body-line-height)20px
--wl-type-body-weight

Descriptions, instructions and form text.

semantic
var(--wl-font-body-weight)400
--wl-type-body-family

Descriptions, instructions and form text.

semantic
var(--wl-font-body-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-small-size

Captions and supporting text.

semantic
var(--wl-font-small-size)12px
--wl-type-small-line-height

Captions and supporting text.

semantic
var(--wl-font-small-line-height)18px
--wl-type-small-weight

Captions and supporting text.

semantic
var(--wl-font-small-weight)400
--wl-type-small-family

Captions and supporting text.

semantic
var(--wl-font-small-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-label-size

Short action or field label.

semantic
var(--wl-font-label-size)13px
--wl-type-label-line-height

Short action or field label.

semantic
var(--wl-font-label-line-height)20px
--wl-type-label-weight

Short action or field label.

semantic
var(--wl-font-label-weight)500
--wl-type-label-family

Short action or field label.

semantic
var(--wl-font-label-family)"Gavia Sans Core", "Gavia Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif
--wl-type-code-size

Tokens, identifiers and technical values.

semantic
var(--wl-font-code-size)12px
--wl-type-code-line-height

Tokens, identifiers and technical values.

semantic
var(--wl-font-code-line-height)18px
--wl-type-code-weight

Tokens, identifiers and technical values.

semantic
var(--wl-font-code-weight)400
--wl-type-code-family

Tokens, identifiers and technical values.

semantic
var(--wl-font-code-family)"SF Mono", ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace
--wl-text-muted

Supporting text with contrast of at least 4.5:1.

semantic
var(--wl-text-2)#5b6470
--wl-focus-color

Visible keyboard focus outline.

semantic
var(--wl-accent)#3c7490
--wl-focus-invalid-color

Focus outline for an invalid field.

semantic
var(--wl-danger-hover)#963c3f
--wl-text-danger

Readable error text on the primary surface.

semantic
var(--wl-err-text)#ab4448
--wl-action-primary-bg

Primary action surface.

semantic
var(--wl-palette-action-primary-bg)#3c7490
--wl-action-primary-hover

Primary action on hover.

semantic
var(--wl-palette-action-primary-hover)#376b84
--wl-action-primary-text

Readable primary action text.

semantic
var(--wl-palette-action-primary-text)#ffffff
--wl-action-danger-bg

Destructive action surface.

semantic
var(--wl-palette-danger)#ab4448
--wl-action-danger-hover

Destructive action on hover.

semantic
var(--wl-palette-danger-hover)#963c3f
--wl-action-danger-text

Readable destructive action text.

semantic
var(--wl-on-accent)#ffffff
--wl-layout-page-max

Page container width.

semantic
var(--wl-size-page-max)1240px
--wl-layout-page-gutter

Content spacing from the page edge.

semantic
var(--wl-space-xl)24px
--wl-layout-grid-gap

Responsive grid gap.

semantic
var(--wl-space-lg)16px
--wl-elevation-surface

Subtle card elevation.

semantic
var(--wl-shadow-card)0 1px 3px rgba(15, 26, 35, 0.05)
--wl-elevation-floating

Floating surface elevation.

semantic
var(--wl-shadow-pop)0 4px 14px rgba(15, 26, 35, 0.08), 0 16px 44px rgba(15, 26, 35, 0.12)
--wl-motion-fast

Button and field feedback.

semantic
var(--wl-dur-2)0.12s
--wl-motion-normal

Small panel entrance.

semantic
var(--wl-dur-4)0.18s
--wl-motion-slow

Large modal transition.

semantic
var(--wl-dur-5)0.2s
--wl-motion-ease

Motion curve.

semantic
var(--wl-ease)ease
--wl-layout-reading-max

Narrow container width for articles and instructions.

semantic
var(--wl-size-reading-max)72ch
--wl-layout-grid-min

Minimum wl-grid column width; capped at 100% inside a narrow container.

semantic
var(--wl-size-grid-min)240px
--wl-layer-base

Regular positioned content layer within its stacking context.

semantic
var(--wl-z-base)0
--wl-layer-sticky

Sticky header or navigation layer; does not escape its parent stacking context.

semantic
var(--wl-z-sticky)50
--wl-action-primary-active

Pressed primary action surface.

semantic
var(--wl-palette-action-primary-active)#326179
--wl-accent-warm

Restrained warm accent for illustrations, decorative markers and small details.

semantic
var(--wl-palette-accent-warm)#948775
--wl-accent-warm-soft

Decorative warm highlight background; use the primary text role for text.

semantic
var(--wl-palette-accent-warm-soft)#efebe3
--wl-corner-button

Default button radius, independent of fields and cards.

semantic
var(--wl-dimension-btn-radius)4px
--wl-corner-button-sm

Radius for xs and sm buttons.

semantic
var(--wl-dimension-btn-radius-sm)3px
--wl-corner-button-lg

Radius for lg buttons.

semantic
var(--wl-dimension-btn-radius-lg)5px

05 / Components

Public contracts

Props, models, events and slots come from the library manifest. Size and density control geometry; variant conveys the action's meaning; states reflect availability.

Check the value.

Use one primary action per group. Danger is for irreversible actions with confirmation. Loading prevents repeated submissions; explain disabled actions nearby.

WlButton

Since 0.1.0

Action button with variants, sizes and loading state.

PropType / valuesPurpose
variantprimary | secondary | ghost | soft | linkVisual button variant.

Default: "secondary"

toneneutral | accent | info | success | warning | dangerSemantic color, independent of the visual treatment.
sizexs | sm | md | lgButton size.

Default: "md"

densitydefault | compactCompact mode reduces button height.

Default: "default"

loadingbooleanShows loading instead of the icon and blocks clicks.

Default: false

disabledbooleanDisables the button.

Default: false

blockbooleanFill the container width.

Default: false

typebutton | submit | resetNative button type.

Default: "button"

ptobjectAttributes of internal Gavia UI elements.
asunionRender a link explicitly; href or to also selects link rendering.
hrefstringLink destination, forwarded to WlLink.
tounionApplication router destination handled by the configured link adapter.
iconPositionunionPlace the icon before or after the default slot.

Slots

  • default — Text or other button content.
  • icon — Leading icon (hidden while loading).

Events

  • click — MouseEvent

Example

variant
tone
size
density

Clicks: 0

Focus: outside the example · pointer: outside the example

Check hover and clicks with a mouse, and focus with the keyboard. Selection, opening and closing update v-model. Settings above can be combined.

Variants and states · 22
Show Vue code
<script setup lang="ts">
import { WlButton, WlIcon } from "gavia-ui";
import { ref } from "vue";
const count = ref(0);
</script>

<template>
  <div class="wl-stack" data-space="md">
    <div>
      <WlButton  @click="count++"><template #icon><WlIcon name="plus" /></template>{{ "Add" }}</WlButton>
    </div>
    <p role="status" class="wl-text-small">{{ "Clicks:" }} {{ count }}</p>
  </div>
</template>

06 / Patterns

Forms, data and panels

Components handle rendering and interaction. The application owns validation, requests and business rules. Examples on this page work without a server.

Form with validation

What should we call you?
For example: name@example.com

Form rules

  1. Connect the visible label to the control using id.
  2. Connect hints and errors using aria-describedby.
  3. Focus the first invalid field on submission.
  4. Keep entered data after an error.
  5. Explain the action's result in text.

WlField provides id, invalid and ariaDescribedby through a scoped slot. The placeholder supplements the label.

Four data states

MaterialState
Typography rulesReady
Token catalogReady
Component contractsReady

Action in a side panel

Drawer opens over the page. Escape closes the panel and restores focus to the trigger. Configure animation for the application or an individual panel.

07 / Recipes

Workflow examples

Six examples with copyable code: data states, cancellation, errors and retries. Workflows are checked alongside components.

Search, filters, sorting and pagination. Create and edit in Drawer, confirm deletion and show Toast notifications.

Team materials

Documents, agreements and research.
12 materials
Material filters
MaterialStatusActions
Access and rolesIn progress
Access and roles · 12In progress
Access and roles · 8In progress
Research planIn progress

Found: 12 · page 1 of 3

Show Vue code
<script setup lang="ts">
import { useWlConfirm, useWlToast, WlButton, WlDrawer, WlEmpty, WlField, WlFilterBar, WlInput, WlPageHeader, WlPagination, WlPill, WlSelect, WlTable, type WlTableColumn, WlTag } from "gavia-ui";
import { computed, nextTick, ref, watch } from "vue";
type Material = { id: number; title: string; status: string };
const materials = ref<Material[]>(Array.from({ length: 12 }, (_, index) => ({
  id: index + 1, title: ["Team guide", "Research plan", "Review guidelines", "Access and roles"][index % 4] + (index < 4 ? "" : ` · ${index + 1}`),
  status: index % 2 ? "progress" : "done"
})));
const search = ref("");
const status = ref<string | null>(null);
const order = ref<string | null>("asc");
const page = ref(1);
const confirm = useWlConfirm();
const toast = useWlToast();
const filtered = computed(() => materials.value.filter((item) =>
  item.title.toLocaleLowerCase("ru").includes(search.value.toLocaleLowerCase("ru")) && (!status.value || item.status === status.value))
  .sort((a, b) => a.title.localeCompare(b.title, "ru") * (order.value === "desc" ? -1 : 1)));
const pageCount = computed(() => Math.max(1, Math.ceil(filtered.value.length / 4)));
const visibleRows = computed(() => filtered.value.slice((page.value - 1) * 4, page.value * 4));
watch([search, status, order], () => {
  page.value = 1;
});
watch(pageCount, (count) => {
  page.value = Math.min(page.value, count);
});
const columns: readonly WlTableColumn<Material>[] = [{ key: "title", label: "Material" }, { key: "status", label: "Status" }, { key: "actions", label: "Actions", kind: "virtual" }];
function clear(): void {
  search.value = "";
  status.value = null;
}
const editing = ref(false);
const editingId = ref<number | null>(null);
const title = ref("");
const titleError = ref("");
let nextId = 13;
function edit(item?: Material): void {
  editingId.value = item?.id ?? null;
  title.value = item?.title ?? "";
  titleError.value = "";
  editing.value = true;
}
async function save(): Promise<void> {
  titleError.value = title.value.trim() ? "" : "Enter a title.";
  if (titleError.value) {
    await nextTick();
    document.getElementById("recipe-material-title")?.focus();
    return;
  }
  const current = materials.value.find((item) => item.id === editingId.value);
  if (current) current.title = title.value.trim();
  else materials.value.push({ id: nextId++, title: title.value.trim(), status: "progress" });
  editing.value = false;
  toast.ok("Material saved");
}
function remove(id: number): void {
  const item = materials.value.find((entry) => entry.id === id);
  if (!item) return;
  confirm.confirmDanger({
    header: "Delete material?", message: `«${item.title}${"» will be deleted."}`, acceptLabel: "Delete", rejectLabel: "Cancel",
    accept: () => {
      materials.value = materials.value.filter((entry) => entry.id !== id);
      toast.ok("Material deleted");
    }
  });
}
</script>

<template>
  <section class="wl-stack" data-space="lg" aria-label="Material list">
    <WlPageHeader
      title="Team materials"
      description="Documents, agreements and research."
      :heading-level="2"
      size="md"
    >
      <template #meta><WlTag>{{ materials.length }} {{ "materials" }}</WlTag></template>
      <template #actions><WlButton variant="primary" @click="edit()">{{ "Create material" }}</WlButton></template>
    </WlPageHeader>
    <WlInput
      v-model="search"
      type="search"
      placeholder="Find a material"
      aria-label="Find a material"
    />
    <WlFilterBar
      :active-count="Number(Boolean(status))"
      aria-label="Material filters"
      panel-title="Material filters"
      @clear="clear"
    >
      <WlSelect
        v-model="status"
        :options="[{ label: 'Done', value: 'done' }, { label: 'In progress', value: 'progress' }]"
        option-label="label"
        option-value="value"
        aria-label="Material status"
        placeholder="All statuses"
      />
      <WlSelect
        v-model="order"
        :options="[{ label: 'Name A–Z', value: 'asc' }, { label: 'Name Z–A', value: 'desc' }]"
        option-label="label"
        option-value="value"
        aria-label="Material order"
      />
      <template #summary><WlTag
        v-if="status"
        removable
        remove-label="Remove status filter"
        @remove="status = null"
      >{{ status === 'done' ? "Done" : "In progress" }}</WlTag></template>
    </WlFilterBar>
    <div
      v-if="filtered.length"
      class="ds-material-table"
      role="region"
      aria-label="Team materials"
      tabindex="0"
    ><WlTable :columns="columns" :value="visibleRows" :pt="{ root: { style: { minWidth: '640px' } } }">
      <template #cell-status="{ value }">
        <WlPill
          :tone="value === 'done' ? 'success' : 'info'"
          :label="value === 'done' ? 'Done' : 'In progress'"
        />
      </template>
      <template #cell-actions="{ row }"><div class="wl-inline" data-space="xs"><WlButton
        size="sm"
        variant="ghost"
        :aria-label="`${'Edit '}${row.title}`"
        @click="edit(materials.find((item) => item.id === row.id))"
      >{{ "Edit" }}</WlButton><WlButton
        size="sm"
        variant="ghost"
        tone="danger"
        :aria-label="`${'Delete '}${row.title}`"
        @click="remove(Number(row.id))"
      >{{ "Delete" }}</WlButton></div></template>
    </WlTable></div>
    <WlEmpty
      v-else
      icon="search"
      title="No materials found"
      description="Change the query or clear the filters."
    ><template #action><WlButton @click="clear">{{ "Clear search" }}</WlButton></template></WlEmpty>
    <div class="wl-inline" data-space="md">
      <WlPagination v-if="filtered.length" v-model:page="page" :page-count="pageCount" />
      <p class="wl-text-small wl-text-muted" role="status">
        {{ "Found:" }} {{ filtered.length }} {{ "· page" }}
        {{ page }} {{ "of" }} {{ pageCount }}
      </p>
    </div>
    <WlDrawer v-model:visible="editing" :header="editingId ? 'Edit material' : 'New material'">
      <form
        id="recipe-material-form"
        class="wl-stack"
        data-space="lg"
        @submit.prevent="save"
      >
        <WlField
          id="recipe-material-title"
          v-slot="field"
          label="Title"
          required
          :error="titleError"
        ><WlInput
          :id="field.id"
          v-model="title"
          :invalid="field.invalid"
          :aria-describedby="field.ariaDescribedby"
          maxlength="120"
        /></WlField>
        <p class="wl-text-small wl-text-muted">{{ "To apply changes, click Save." }}</p>
      </form>
      <template #footer>
        <WlButton variant="ghost" @click="editing = false">{{ "Cancel" }}</WlButton>
        <WlButton variant="primary" type="submit" form="recipe-material-form">{{ "Save" }}</WlButton>
      </template>
    </WlDrawer>
  </section>
</template>

<style scoped>
.ds-material-table { overflow: auto; max-width: 100%; }
</style>

Examples use sample data. Saving and loading are simulated in the browser. Connect server requests in your application. The workflow and copyable code share one Vue file.

Styles, theme and services setup
// main.ts — Import CSS explicitly; set the theme using data-wl-theme on html.
import { createApp } from "vue";
import { WlConfig, WlToastService, WlConfirmationService, createWlPt, wlLocaleEn } from "gavia-ui";
import "gavia-ui/styles/reset.css";
import "gavia-ui/styles/base.css";
import "gavia-ui/styles/primitives.css";
import "gavia-ui/themes/white.css";
import "gavia-ui/themes/graphite.css";
import "gavia-ui/themes/newspaper.css";
import "gavia-ui/styles/fonts/gavia.css";
import "gavia-ui/themes/gavia.css";
import "gavia-ui/themes/gavia-dark.css";
import App from "./App.vue";

document.documentElement.dataset.wlTheme = "white";

// WlConfig merges application pt with defaults; local component pt is applied last.
const pt = createWlPt({ button: { root: { "data-ui": "action" } } });
createApp(App).use(WlConfig, { locale: wlLocaleEn, motion: true, pt })
  .use(WlToastService).use(WlConfirmationService).mount("#app");

// App.vue: import WlToast/WlConfirmDialog and mount one of each
// container alongside the page. Notifications and confirmations use them.
// <template><YourPage /><WlToast /><WlConfirmDialog /></template>

08 / Complex content

Long text and large lists

Long text, eight tags, 80 options, 20 table rows, date limits and nested overlays. Change the window width and text size.

Agreements for an international distributed team preparing materials and conducting research together

Narrow the window to see how the heading, buttons and metadata wrap.
Research 1Research 2Research 3Research 4Research 5Research 6Research 7Research 8
8 selected
Give the material a clear name so people from different teams can find it through search and distinguish it from similar materials.
October 1–31, 2026.
Long nameIdentifier
Agreements for an international distributed team preparing materials and conducting research together · 1research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 2research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 3research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 4research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 5research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 6research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 7research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 8research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 9research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 10research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 11research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 12research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 13research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 14research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 15research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 16research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 17research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 18research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 19research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz
Agreements for an international distributed team preparing materials and conducting research together · 20research_material_without_spaces_2026_10_01_abcdefghijklmnopqrstuvwxyz

09 / Accessibility

Keyboard, contrast and focus

Text contrast starts at 4.5:1; focus indicators at 3:1. The report checks the listed token pairs. Check custom colors and complete screens separately.

Keyboard

Tab moves focus. Enter / Space activates actions. Arrows change selection. Escape closes overlays. Focus stays visible and returns after closing.

Motion

motion: false disables transitions. The system prefers-reduced-motion setting applies to every theme. Animation does not change the meaning or lifetime of data.

Text and target size

Give icon actions an aria-label. Errors should explain the next step. Aim for 44 × 44 px touch targets; use lg with sufficient spacing.

Pair / gaviaActualMinimum
Body text16.73:14.5:1
Supporting text5.7:14.5:1
Text on a secondary surface15.63:14.5:1
Primary action5.13:14.5:1
Primary action: hover5.84:14.5:1
Primary action: active6.73:14.5:1
Destructive action5.75:14.5:1
Destructive action: hover6.97:14.5:1
Tooltip17.61:14.5:1
Information message7.8:14.5:1
Success message5.84:14.5:1
Warning5.73:14.5:1
Error message5.04:14.5:1
Focus on the page4.87:13:1
Link on the page6.39:14.5:1
Focus on a secondary surface4.55:13:1
Text on a raised surface17.33:14.5:1
Supporting text on a raised surface5.9:14.5:1
Focus on a raised surface5.05:13:1
Accent text on a raised surface6.62:14.5:1
Accent text on a secondary surface5.97:14.5:1
Accent text on a selected surface5.41:14.5:1
Accent text on a soft surface: hover4.95:14.5:1

New tokens pass tokens:check. Patterns are checked in Chromium, Firefox, WebKit and mobile Chromium. Before release, types, the package archive and an isolated consumer are checked too.

Gavia UI · Vue 3 · component API · documentation in docs/design-system.md