Overview
Theming
@lumiose/ui is unstyled: its stylesheets hold only what the components need to work. The optional default theme gives them a look, light and dark, through --lu- custom properties you can change; or style the parts with your own CSS.
#Cascade layers
The components' stylesheets hold only what a component needs to work (display, positioning and
anchoring, the top layer, the CSS variables a component sets), in the lu.core cascade layer. The
default theme sits in lu.theme, above it:
import "@lumiose/ui/styles.css"; // what every component needs (or "@lumiose/ui/dialog.css", …)
import "@lumiose/ui/themes/default.css"; // optional: the default themeBoth stylesheets declare the layers lu.core, lu.theme, in that order. CSS outside layers always
wins over layered CSS, so your own stylesheets override the package's without specificity games.
If your app orders its own layers, put lu first: @layer lu, reset, components;.
#The default theme
@lumiose/ui/themes/default.css is a ready look for demos, prototypes and getting started: neutral
surfaces, a blue primary, light and dark. It styles a few classes, which you put on the parts
through their class prop:
| Class | On | Variants |
|---|---|---|
lu-button |
Button, any <button> part |
data-variant="primary", data-variant="destructive" |
lu-dialog |
Dialog.Popup |
Styles the backdrop, the title, the description and a <footer> inside |
lu-popover |
Popover.Popup |
Styles the title, the description and a <footer> inside |
lu-tooltip |
Tooltip.Popup |
|
lu-tabs |
Tabs.Root |
Styles the list, the tabs (the selected one underlined) and the panels |
lu-menu |
every Menu.Popup |
Styles the items, their checkbox and radio marks, separators and labels |
lu-combobox |
Combobox.Root, Autocomplete.Root |
Lays out the field (label, chips, input, buttons) and styles the popup |
lu-select |
Select.Root |
Makes it a customizable select and styles the control and the picker |
lu-field |
Field.Root |
Stacks the label, control, description and errors; errors in red |
lu-input |
Input, Field.Control |
A text field with a focus ring and a red border when invalid |
lu-fieldset |
Fieldset.Root, CheckboxGroup, RadioGroup |
The legend and the items in a column |
lu-checkbox |
Checkbox.Root |
An 18px box in a 24px target, a ✓ or a dash |
lu-radio |
Radio.Root |
An 18px circle in a 24px target, a thick ring when checked |
lu-switch |
Switch.Root |
A track and a thumb on the side that tells on from off |
lu-collapsible |
Collapsible.Root |
Styles the trigger (a chevron) and animates the panel's height |
lu-accordion |
Accordion.Root |
Items between borders; styles the triggers and animates the panels |
lu-toast-viewport |
Toast.Viewport |
A column at the window's bottom end |
lu-toast |
every Toast.Root |
Styles the title, description and close button; data-type="error" |
lu-toggle |
Toggle |
Pressed: a filled surface and a bar along the bottom |
lu-toggle-group |
ToggleGroup |
The toggles in a row, or a column when vertical |
lu-progress |
Progress.Root |
The label and value above a bar; a sliding segment when indeterminate |
lu-meter |
Meter.Root |
The label and value above a bar |
lu-slider |
Slider.Root |
The label and value above the track, the indicator and 24px thumbs |
lu-number-field |
NumberField.Root |
The label above one bordered group: the input between its buttons |
lu-otp-field |
OTPField.Root |
A box per character, the caret's box ringed while focused |
lu-preview-card |
PreviewCard.Popup |
A surface like a popover's, with a fade |
lu-toolbar |
Toolbar.Root |
A bordered row (or column), separators across it, links as tall as buttons |
lu-scroll-area |
ScrollArea.Root |
A bordered box; 10px scrollbars shown on hover, scroll or focus |
lu-drawer |
Drawer.Popup |
A surface against its edge (a handle on a bottom sheet), title, description, footer; slides |
lu-separator |
Separator |
A 1px line in --lu-border, horizontal or vertical |
lu-avatar |
Avatar.Root |
A 40px circle on a muted surface, the image covering it, the initials centred |
lu-menubar |
Menubar |
A bordered bar of the triggers, the open one filled; lu-menu on each popup |
lu-navigation-menu |
NavigationMenu.Root |
The list in a row, the current page underlined, contents as surfaces that slide in |
Its colours and radius are custom properties with shadcn/ui's names and a --lu- prefix, so a
shadcn palette carries over:
| Property | Used for |
|---|---|
--lu-background, --lu-foreground |
Page-level surfaces and text |
--lu-card, --lu-card-foreground |
Cards |
--lu-popover, --lu-popover-foreground |
Dialogs, popovers, menus |
--lu-primary, --lu-primary-foreground |
Primary actions |
--lu-secondary, --lu-secondary-foreground |
Secondary actions |
--lu-muted, --lu-muted-foreground |
Subdued surfaces and text |
--lu-accent, --lu-accent-foreground |
Hover and highlighted items |
--lu-destructive, --lu-destructive-foreground |
Destructive actions |
--lu-border, --lu-input, --lu-ring |
Borders, field borders, focus rings |
--lu-radius |
Corner radius |
| Property | Light | Dark |
|---|---|---|
--lu-background | #fff | #09090b |
--lu-foreground | #09090b | #fafafa |
--lu-card | #fff | #18181b |
--lu-card-foreground | #09090b | #fafafa |
--lu-popover | #fff | #18181b |
--lu-popover-foreground | #09090b | #fafafa |
--lu-primary | #2563eb | #2563eb |
--lu-primary-foreground | #fff | #fff |
--lu-secondary | #f4f4f5 | #27272a |
--lu-secondary-foreground | #18181b | #fafafa |
--lu-muted | #f4f4f5 | #27272a |
--lu-muted-foreground | #67676f | #a1a1aa |
--lu-accent | #f4f4f5 | #27272a |
--lu-accent-foreground | #18181b | #fafafa |
--lu-destructive | #dc2626 | #dc2626 |
--lu-destructive-foreground | #fff | #fff |
--lu-border | #e4e4e7 | #3f3f46 |
--lu-input | #85858e | #71717a |
--lu-ring | #2563eb | #60a5fa |
--lu-radius | 0.625rem | 0.625rem |
The theme sets color-scheme: light dark on :root and every colour with light-dark(), so it
follows the system. Set color-scheme: light (or dark) on :root or on any element, in CSS,
inline or from a script, to force one, in development and in production builds alike (Lumiose's
CSS target keeps light-dark() as written; an older build.cssTarget would lower it to colours
that follow the system only). Redefine a property outside layers to change it:
:root {
--lu-primary: #7c3aed;
--lu-radius: 0.375rem;
}The theme meets WCAG 2.2 AA in light and dark: each -foreground colour reaches 4.5:1 on its
surface, and --lu-input (the borders of fields and controls) and --lu-ring (the focus ring)
reach 3:1 on the surfaces they sit on. --lu-border is for decoration, such as a dialog's edge:
it is too light to outline a control. Keep those ratios when you change the colours.
In forced colours (Windows' contrast themes), the browser drops shadows and gradients and paints
backgrounds in its own palette, which would erase the state a colour shows. The theme draws those
states again in system colours (Highlight, HighlightText, ButtonText, Canvas): a switch's
thumb and its on state, a pressed toggle, the selected tab's bar, progress and meter values, a
slider's range, the scroll thumb and the highlighted menu item. A pressed toggle's focus ring,
outside its Highlight fill, is CanvasText, the colour every contrast theme pairs with the page
(Firefox would draw it in the toggle's HighlightText). Do the same for the states your own CSS
draws with a colour, in a @media (forced-colors: active) block.
#Your own CSS
Style the parts through:
- Class props. Every part takes
class(andstyle), like the element it renders. - State. Components expose state the way the platform does, so selectors read it directly:
dialog[open],:modal,::backdrop,:popover-open,[aria-expanded],[aria-checked],[aria-selected]. Where the platform has no such state, they set Base UI's state attributes:data-highlightedon the active option of a list,data-sideanddata-alignon a placed popup,data-orientation,data-starting-styleanddata-ending-stylewhile a popup animates,data-swiping,data-indeterminateand others. Each component's page lists its own. - Custom properties. The values components measure or place, such as a panel's height, a
toast's index in its stack or a popup's offset, are custom properties with the
--lu-prefix. Base UI names them without a prefix, so its--collapsible-panel-heightis--lu-collapsible-panel-heighthere and its--scroll-area-thumb-heightis--lu-scroll-area-thumb-height.
Other data-* attributes on the rendered HTML, such as the markers scripts use to find a part or
the settings the server HTML passes to a script, are internal and may change between versions.
Style those parts through their class props.
Each component's page has an example stylesheet to start from.
#Related
- UI components: the approach and the components.
- Installation: adding the package and importing its stylesheets.
- Styles: scoped CSS and global stylesheets in Lumiose.