@carissa-io/theme-default
The Carissa Default theme: Swiss minimalism (square corners, absolute black for primary actions and focus, a neutral grey scale, a neutral grotesque type stack). The Carissa components ship neutral defaults and no look of their own; this package gives them one by overriding their SCSS variables. The decisions behind the values are in DESIGN.md.
Install
npm install @carissa-io/theme-default
The compiled CSS needs nothing else. To compile the SCSS yourself, also install the components you use (they are optional peer dependencies).
Use it
- Compiled CSS (nothing to compile; the components must still be installed to render):
It is also the package's main export, soimport '@carissa-io/theme-default/css'import '@carissa-io/theme-default'works with a bundler that handles CSS. - SCSS source: forward or
@useit from your own Sass entry, with@carissa-io/*resolvable (node_moduleson the load path):@use '@carissa-io/theme-default/scss'; - Tokens (Sass variables only, no CSS): see Tokens.
- A starting point for a new theme: copy
scss/index.scssandscss/_tokens.scssand change the values. In the Carissa workspace,scripts/new-theme.sh <name> "<description>"does this and sets up the repo.
Color modes
The theme has a light and a dark palette in one package. The palette is a set of CSS custom properties (--c-background, --c-text,
--c-muted, --c-surface, --c-surface-active, --c-primary, --c-on-primary, --c-primary-hover, --c-primary-active,
--c-disabled-text, --c-disabled-border, --c-error, --c-success, --c-warning, --c-info, --c-focus, --c-scrollbar-thumb, --c-scrollbar-track) that every component reads. Dark is opt-in per app: without
the attribute below the app is light, exactly as before.
An app opts in with one line in the file where it imports the theme (App/theme.js), setting the mode on <html>:
import '@carissa-io/theme-default/css'
document.documentElement.dataset.mode = 'auto' // 'auto' | 'dark' | 'light'
data-mode on <html> | Result |
|---|---|
| none | light |
light | light |
dark | dark |
auto | light, and dark while the system asks for it (prefers-color-scheme: dark) |
color-scheme follows the mode, so native controls and scrollbars match. To avoid a flash of the wrong mode, set the attribute before the
first paint: put this in the <head> of the page, before the stylesheet, with the choice the app remembers (here in localStorage):
<script>
try {
document.documentElement.dataset.mode = localStorage.getItem('mode') || 'auto'
} catch {
document.documentElement.dataset.mode = 'auto'
}
</script>
The Sass tokens above stay the static light values (for Sass consumers); the dark values are in scss/_modes.scss. The page background and
text color are the app's: give body background: var(--c-background); color: var(--c-text).
Scrollbars
The theme styles every scroll container with no change in any component: the page (the viewport bar follows), the message list, the table
wrapper, the modal and drawer bodies, the select, combobox and listbox popups, the sidebar and code blocks. The two palette roles
--c-scrollbar-thumb and --c-scrollbar-track follow data-mode.
- Chrome, Edge, Safari and other Blink / WebKit browsers get an 8px bar (
$scrollbar-size) with a square thumb ($scrollbar-radius, which is the shared$radius, so a theme with rounded corners gets a rounded thumb) and no arrow buttons, drawn with the*::-webkit-scrollbarpseudo-elements. The standardscrollbar-colorandscrollbar-widthare deliberately not used there: from Chrome 121 they switch the bar to the standardized rendering (rounded thumb, arrows can remain) and make the pseudo-elements ignored. - Firefox has no such pseudo-elements, so it gets the standard properties (
scrollbar-coloron:root,scrollbar-width: thinfrom$scrollbar-widthon*), coloured with the same roles. Their shape cannot be styled: the thumb ends are not square there. - Older engines keep the native bar.
All the rules have zero or near-zero specificity (* and *::-webkit-scrollbar...), so any component rule or your own rule wins over them; the
carousel already hides its own bar.
To change them in your own theme, set $scrollbar-thumb, $scrollbar-track, $scrollbar-size, $scrollbar-radius and $scrollbar-width in
your scss/_tokens.scss (and the dark values of the two roles in scss/_modes.scss); keep the thumb at 3:1 or more against the track and against
the background. The default track is the page background, so inside a surface-coloured container (the sidebar, a card) it shows as a strip of the
page colour; a theme that prefers it quieter there sets $scrollbar-track to its surface value. An app that only needs one scroller to look
different overrides ::-webkit-scrollbar-thumb (and scrollbar-color for Firefox) on that element. See DESIGN.md for the values and limits.
Tokens
The palette and the values shared by every component are Sass variables in scss/_tokens.scss, the single source of
the theme's values (scss/index.scss reads them with @use 'tokens' as *). The file emits no CSS, so an app can read
the values without pulling any rule in. The names are the public token API: renaming or removing one is a breaking
change.
@use '@carissa-io/theme-default/scss/tokens' as theme;
| Token | Value | Use |
|---|---|---|
$black | #000 | primary, hover and focus |
$white | #fff | surfaces |
$text | #171717 | body text |
$muted | #666666 | labels, field borders, placeholders |
$surface | #f5f5f5 | hover surface, disabled background |
$surface-active | #e5e5e5 | pressed surface, dividers |
$primary-hover | #262626 | primary action on hover |
$primary-active | #404040 | primary action while pressed |
$disabled-text | #a3a3a3 | disabled text and marks |
$disabled-border | #d4d4d4 | disabled borders |
$error | #dc3545 | error text and borders |
$success | #15803d | success accent and icon (Alert) |
$warning | #a16207 | warning accent and icon (Alert) |
$info | #1d4ed8 | info accent and icon (Alert) |
$scrollbar-thumb | #737373 | scrollbar thumb |
$scrollbar-track | $white | scrollbar track (the page ground) |
$scrollbar-width | thin | scrollbar thickness where the standard property is used (Firefox) |
$scrollbar-size | 0.5rem | scrollbar thickness in Blink and WebKit |
$scrollbar-radius | $radius | scrollbar thumb radius in Blink and WebKit (square here) |
$font-family | 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif | type stack |
$font-family-mono | ui-monospace, 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', monospace | monospace stack for system data and code |
$radius | 0 | corner radius |
$min-height | 2.75rem | touch target |
$focus-color | $black | focus color |
$focus-width | 1px | focus ring width |
$transition-duration | 0.2s | color and border transitions |
$motion-duration-short | 0.25s | quick entrances and small moves (scramble settle, four of them make the typewriter cursor blink) |
$motion-duration-medium | 0.5s | the default entrance (animate) and the highlight-sweep |
$motion-duration-long | 0.9s | counts (counter), reveals and other slow moves |
$motion-easing | cubic-bezier(0.16, 1, 0.3, 1) | the one decelerating easing of the motion components |
$motion-stagger | 0.15s | gap between the items of a sequence or a stagger-list |
$label-font-size | 0.6875rem | field label size |
$label-font-weight | 700 | field label weight |
$label-letter-spacing | 0.1em | field label tracking |
$font-size-display | 3.5rem | Heading display |
$font-size-h1 | 2.5rem | Heading 1 |
$font-size-h2 | 1.75rem | Heading 2 |
$font-size-h3 | 1.25rem | Heading 3 |
$font-size-h4 | 1.125rem | Heading 4 |
$font-size-h5 | 1rem | Heading 5 |
$font-size-h6 | 0.875rem | Heading 6, Text mono |
$font-size-lead | 1.25rem | Text lead |
$font-size-body | 1rem | body text, and the text of every field and option (16px) |
$font-size-small | 0.875rem | Text small |
$font-size-caption | 0.75rem | Text caption |
$font-size-overline | $label-font-size | Text overline |
$font-weight-regular | 400 | body text |
$font-weight-bold | 700 | titles from h3, selected option |
$font-weight-black | 900 | display, h1 and h2 |
$line-height-tight | 1.3 | h3 and h4 |
$line-height-normal | 1.5 | running text |
$arrow-size | 1rem | dropdown arrow size |
$arrow-offset | 0.75rem | dropdown arrow inset |
$arrow-color | $muted | dropdown arrow color |
$arrow-color-active | $black | dropdown arrow on hover and when open |
$arrow-color-disabled | $disabled-text | dropdown arrow when disabled |
$arrow-transition | rotate and color, $transition-duration | dropdown arrow motion |
$arrow-rotate-open | 180deg | dropdown arrow rotation when open |
Recommended for apps: configure Bootstrap from the tokens, so the app and the Carissa components share one palette.
Needs sass-embedded (or sass) and a bundler whose Sass importer respects package exports (Vite does):
@use '@carissa-io/theme-default/scss/tokens' as theme;
@use 'bootstrap/scss/bootstrap' with (
$primary: theme.$black,
$font-family-sans-serif: theme.$font-family,
$body-bg: theme.$surface
);
Outside a bundler, put node_modules on the Sass load path (sass --load-path=node_modules).
Every theme repo has its own tokens, with the same names. Switching theme means the same @use path with another package
name (@carissa-io/theme-<name>/scss/tokens). A theme that does not define a token must not be used with code that expects
it: check the names against the theme's scss/_tokens.scss before you switch.
What it configures
components-input, -button, -checkbox, -switch, -radio-group, -slider, -popover, -listbox, -select,
-combobox, -upload, -calendar, -date-picker, -search, -typography, -card, -alert, -spinner, -modal, -toast, -divider, -badge, -tag, -link, -avatar, -skeleton, -progress, -tooltip, -prose, -tabs, -accordion, -breadcrumb, -pagination, -stepper, -sidebar, -navbar, -drawer, -menu, -table, -list, -description-list, -stat, -empty-state, -timeline, -tree, -code, -field, -number-input, -otp-input, -phone, -tag-input, -time-picker, -date-range, -rating, -color-picker, -async-autocomplete, -section, -faq, -carousel, -visually-hidden, -live-region, -theme-switch, -icon, -ai-label, -thinking-indicator, -streaming-text, -message, -message-list, -prompt-input, -suggested-prompts, -message-actions, -animate, -sequence, -typewriter, -scramble, -counter, -highlight-sweep, -ticker, -stagger-list, -demo, -feature-card and -device-frame. components-email, -password, -url and -textarea
are styled through the input.
The checkbox's mixed state (indeterminate, from the next components-checkbox release, class c-checkbox--indeterminate) is the checked box with a 10px dash in --c-on-primary; on an older checkbox the rule is simply unused.
Change a value
The palette and the values shared by every component (fonts, radius, minimum height, focus ring, transition, label style, dropdown
arrow) are the Sass variables of scss/_tokens.scss (the tokens); each component has one @use ... with (...) block of overrides
below. Change the value, run npm run build, and update DESIGN.md if the decision changes.
Develop
npm install
npm run lint # compile, fail on any Sass error or warning
npm test # builds, then checks every component root has rules and dist/ is current
npm run build # dist/index.css
dist/ is not committed; it is built before every publish. The package ships dist, scss and DESIGN.md. Releases
follow the Carissa project rules: branch and pull request, CI publishes when the version on main is new.