@voraus/mui-theme
v0.3.2
Published
The voraus brand as a Material UI theme
Readme
@voraus/mui-theme
The voraus brand as a Material UI theme. This is what makes a React application look like voraus without its component code being touched.
import { ThemeProvider } from '@mui/material/styles'
import { vorausTheme } from '@voraus/mui-theme'
;<ThemeProvider theme={vorausTheme}>{children}</ThemeProvider>vorausThemeOptions is exported beside it, for an application that extends the theme before
building it.
Light and dark
The theme has a light and a dark color scheme. Both name the same tokens, and the tokens change with
data-theme, so the two differ only in palette.mode, which Material UI branches on. The
ThemeProvider owns that attribute: it writes data-theme="light" or "dark" onto <html>,
which is the selector the dark token stylesheet is published under, and it keeps the mode in local
storage as mui-mode. A toggle calls useColorScheme().setMode():
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript'
import { ThemeProvider, useColorScheme } from '@mui/material/styles'
import { vorausTheme } from '@voraus/mui-theme'
function Toggle() {
const { mode, setMode } = useColorScheme()
// `mode` is undefined until the provider has read the stored one.
if (!mode) return null
return <button onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')}>{mode}</button>
}
;<>
<InitColorSchemeScript attribute="data-theme" />
<ThemeProvider theme={vorausTheme}>
<Toggle />
{children}
</ThemeProvider>
</>InitColorSchemeScript goes into the <head> or at the top of <body>, so the attribute is set
before the first paint and a page stored as dark never flashes light. Its attribute has to name
the same attribute as the theme. Without a stored mode, both follow prefers-color-scheme.
Since the provider writes the attribute, it is the only thing that may. Docusaurus sets
data-theme itself and renders no Material UI theme, so nothing here touches it. A host that owns
the attribute and mounts this theme as well hands the provider a storageManager that answers with
the host's mode. The brand platform is that case: .storybook/preview.ts in @voraus/storybook
keeps its page light and shows dark in subtrees that set data-theme themselves.
The stylesheets are the consumer's to load
Every value in this theme is a var(). Nothing here loads the stylesheet that declares those
custom properties, and without it Material UI renders the gradient button with no gradient, black
text, square corners and Times New Roman. An application therefore imports them once, at its entry
point:
import '@voraus/assets/fonts/roboto.css'
import '@voraus/tokens/voraus.css'
import '@voraus/tokens/voraus-dark.css' // only if the application offers a dark theme
import '@voraus/tokens/voraus-light.css' // only to pin a subtree to light on a dark page
import '@voraus/css/voraus.css' // only for the tile, the rule and the plain class namesThis package does not import them for you, because two of them are decisions only the application
can make. Without the dark stylesheet the dark scheme keeps the light values, so whether an
application offers dark at all is up to it. @voraus/css carries a small reset, and a library
should not put one into an existing application without being asked. It is needed for
<Card variant="tile"> and <Divider variant="gradient">, which are drawn by the stylesheet rather
than by this theme.
Every value is a token
palette.primary.main is var(--voraus-color-action-primary), not a hex, so this application and a Sphinx
page loading @voraus/css resolve the same custom properties and switching the theme moves both.
Material UI derives light, dark and contrastText from main when they are missing, and that
derivation parses the color and answers a var() with "Unsupported color". Every role therefore
spells all four out, and the tests check that, because a missing one is a throw in the consuming
application rather than a failure here.
light and dark are hover and active states rather than brand values, so they are mixed off the
role with color-mix. The same goes for the greys Material UI would otherwise fill with rgba
blacks, which vanish on a dark surface.
Material UI still fills what it is not asked about with literals of its own, such as grey,
secondary and the FilledInput background. A component this theme does not style draws with
those, in either mode. The tests check that the dark scheme adds no literal the light one does not
already have.
One variant is added, three are inherited
text, outlined and contained are Material UI's own words, and @voraus/css uses the same three
so a Sphinx page and a React app do not name the same thing twice. gradient is the addition,
declared through module augmentation, so <Button variant="gradient"> is type-safe.
It uses the brand gradient, as vorausrobotik.com sets its primary button. A contained button fills
with action.fill rather than primary, because primary on a dark page takes no white label.
Fields take size="small"
A TextField, a Select or an Autocomplete with size="small" is as high as
control.min-height-small, which each face sets as it sets control.min-height. The resting label
moves in for that height. The small field is for a dense form and for the header of a documentation,
where it stands as high as the icon button beside it, so the header keeps its height.
A button keeps the full height at size="small". A card sets its actions small, and the button of
@voraus/css has one height only, so a small button would break the two apart.
A multiline field grows
A field with multiline takes the height of its rows and its text. Its first line stands where the
text of a single-line field does, a half of control.min-height from the top, or of
control.min-height-small at size="small". One row is therefore as high as a single-line field,
and the resting label sits on that line. The text keeps the inset of a single-line field.
Its corner is control.radius, but never rounder than the curve of a single-line field of the same
size. A pill of three lines would turn into a stadium. The display face gets a rounded rectangle with
the corners of its pill, the application face stays square.
The stepper runs on the gradient
Stepper draws the rail of the steps in @voraus/css: a point per step in the color the gradient has
there, joined by the stretch of the gradient between two points. What the stepper has not reached yet
is grey, so the gradient reads as the progress. The point replaces the numbered circle, because white on
the turquoise end of the gradient does not reach 3:1. The number stands in front of the title instead,
as in @voraus/css.
The stepper is for a flow the user works through. A history or a procedure that is only read takes
VorausSteps from @voraus/react, which draws the same rail with the prose of each step under it.
The theme works the color out from the position of the step with sibling-index(), and registers the
two custom properties it needs with @property, so it needs no stylesheet of @voraus/css. A browser
without sibling-index() draws every point at the start of the gradient.
Tabs and the accordion match the stylesheet
The chosen tab draws its own bar rather than a sliding indicator, and the accordion is square, inside the
one line and without a shadow. @voraus/css draws both the same way, and the brand platform fails a test
where the two differ.
The card is square
Cards are square against the pill buttons and drawn with a line rather than a shadow, as the draft
has them, so elevation: 0 is the default. In dark mode Material UI otherwise lightens a raised
Paper with a white gradient image, which no token reaches. The dark scheme declares no such
overlay, as the light one never had.
