# Adaptive interfaces
The `adaptive` prop enables mobile presentation below the configured breakpoint. Supported controls open a modal suited to their content. It is opt-in; its default is `false`.
## Try it
Use the device buttons to switch between desktop, tablet, and mobile viewports. Open a control to compare its popup and modal presentations. The preview uses a real embedded viewport, so you can try mobile behavior without resizing your browser.
```vue
Actions
{{ message }}
```
## Supported controls
| Component | Adaptive behavior |
| --------------------------------------------------------------------- | ------------------------------------------------------------- |
| [Select](https://selarasui.my.id/components/forms/select) | Selection popup becomes a modal. |
| [Autocomplete](https://selarasui.my.id/components/forms/autocomplete) | A modal contains the input, suggestions, and selected values. |
| [Dropdown](https://selarasui.my.id/components/overlays/dropdown) | Menu actions appear in a modal. |
| [DatePicker](https://selarasui.my.id/components/forms/date-picker) | Date and time picker content appears in a modal. |
| [ColorPicker](https://selarasui.my.id/components/forms/color-picker) | Color controls appear in a modal. |
Models and item actions use the same API in either presentation. Other overlay components do not automatically acquire an `adaptive` prop.
## Breakpoint
The default is Tailwind's `md` breakpoint. Set `selaras.adaptive.breakpoint` in Nuxt configuration to choose another Tailwind breakpoint token. Define its length in CSS; see [Nuxt configuration](https://selarasui.my.id/overview/getting-started/nuxt-configuration#adaptive-breakpoint).
Server rendering starts with desktop presentation. The viewport is measured after mount, and an open adaptive control retains its chosen presentation until it closes. Test both viewport sizes and keyboard interaction in your application.
# Component usage
Selaras components are auto-imported with the `S` prefix. Start with an existing component, then customize its props and slots before building a replacement.
## State and events
```vue
```
Component reference pages document models, events, item shapes, and slots. Item labels are your content; internal labels are controlled through [messages](https://selarasui.my.id/overview/getting-started/messages).
## Customization
Use props for supported behavior and appearance. Use slots for custom content, `class` for the root, and `:ui` for specific styled parts. See [Component styling](https://selarasui.my.id/overview/theming/component-styling) for merging and precedence.
Wrap your app in [SApp](https://selarasui.my.id/components/layout/app) for providers. Add [Toast](https://selarasui.my.id/components/overlays/toast) and [CommandPalette](https://selarasui.my.id/components/overlays/command-palette) where their references describe; providers alone do not render these controls.
## Responsive interfaces
Use [adaptive controls](https://selarasui.my.id/overview/getting-started/adaptive-interfaces) to opt into modal presentation on small screens. Responsive layout utilities and adaptive presentation solve different needs.
# Icons
Selaras uses [Nuxt Icon](https://nuxt.com/modules/icon){rel=""nofollow""} under the hood and installs its module automatically. `SIcon` wraps its `Icon` component, so Nuxt Icon configuration, providers, aliases, and custom collections also apply to Selaras icons. Icon names use `collection:name`; internal component icons default to Hugeicons.
## Local icons
```vue-html
Search
```
Components expose their supported `icon`, `trailingIcon`, or icon slots in their reference pages. Explicit icon names do not change when you override the internal registry. Give icon-only actions an accessible label; decorative icons can be hidden from assistive technology.
## Global internal icons
```ts [app.config.ts]
export default defineAppConfig({
selaras: {
icons: {
close: 'hugeicons:cancel-01',
loading: 'hugeicons:loading-02',
},
},
})
```
Each key describes a purpose rather than a component. Overriding `close` changes components that read that registry key. Unspecified keys retain their defaults. See [useIcons](https://selarasui.my.id/utilities/composables/use-icons) for the complete registry and how custom components can reuse it.
## Configuration and loading
Configure Nuxt Icon through the top-level `icon` key in `nuxt.config.ts`, alongside `selaras`. You do not need to register `@nuxt/icon` again.
Nuxt Icon can fetch icons from the [Iconify](https://iconify.design/){rel=""nofollow""} API when they are unavailable locally. This is convenient for trying collections, but runtime requests can delay an icon's appearance. Install the collections you use locally:
```bash
bun add -d @iconify-json/hugeicons @iconify-json/lucide
```
`devDependencies` is appropriate for the normal bundled build: icon data is consumed during building. Build environments must install dev dependencies. If you explicitly externalize icon JSON for runtime resolution, keep the packages available as production dependencies instead.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
icon: {
serverBundle: {
collections: ['hugeicons', 'lucide'],
},
clientBundle: {
scan: true,
icons: ['lucide:search', 'lucide:x'],
},
},
})
```
Installing a collection avoids relying on the public API; it does not alone guarantee immediate rendering. The client bundle makes known icons available without an icon request. Scanning finds literal names; add dynamically selected names explicitly to `clientBundle.icons`. This is especially useful for static sites without a Nuxt server endpoint.
## Runtime customization
Nuxt Icon's runtime settings belong under `icon` in `app.config.ts`. Selaras's internal-purpose registry remains under `selaras.icons`:
```ts [app.config.ts]
export default defineAppConfig({
icon: {
mode: 'svg',
aliases: {
'app-search': 'lucide:search',
},
},
selaras: {
icons: {
search: 'app-search',
},
},
})
```
Nuxt Icon also supports global SVG customization through `icon.customize`. Selaras components set their own icon sizing classes, so prefer their `ui` slots for sizing individual component parts. See the [Nuxt Icon configuration reference](https://github.com/nuxt/icon#icon-customization){rel=""nofollow""} for the full upstream API.
## Custom SVG collections
Place SVG files in `app/assets/icons`, then register a collection:
```ts [nuxt.config.ts]
import { createResolver } from 'nuxt/kit'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
icon: {
customCollections: [
{ prefix: 'app', dir: resolve('./app/assets/icons') },
],
clientBundle: {
includeCustomCollections: true,
},
},
})
```
A file named `brand.svg` becomes `app:brand`. Including custom collections in the client bundle also makes them available on static hosting.
```vue-html
Workspace
```
## Vue components as icons
Nuxt Icon can resolve a globally registered Vue component by name. For example, create `app/components/global/BrandIcon.vue`:
```vue
```
Then use that component name through Selaras:
```vue-html
Workspace
```
The component must be globally registered; an ordinary local import is insufficient for name-based resolution. Nuxt Icon renders the component directly and ignores its CSS/SVG mode setting for that icon. See [Nuxt Icon's component support](https://github.com/nuxt/icon#vue-component){rel=""nofollow""}.
# Installation
## Requirements
Selaras supports Nuxt `^4.5.2`, Tailwind CSS `^4.3.0`, and Node.js
`^22.19.0 || ^24.11.0 || >=26.0.0`. Nuxt provides the Vue runtime and SFC
compiler, so they are not installed or versioned separately. Type checking is
verified with TypeScript `>=5.9.3`; TypeScript is a build-time tool, not a
Selaras runtime dependency.
## Install the package
Nuxt and Tailwind CSS are peer dependencies - install Selaras in an existing
Nuxt project and keep Tailwind under your project's direct control:
::code-group
```bash [bun]
bun add @sewadah/selaras tailwindcss
```
```bash [npm]
npm install @sewadah/selaras tailwindcss
```
```bash [pnpm]
pnpm add @sewadah/selaras tailwindcss
```
::
## Register the module
Add `@sewadah/selaras` to `modules` in `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
})
```
Components are now auto-imported, and the module pulls in its own
dependencies (`@nuxt/icon`, `@nuxtjs/color-mode`) automatically.
## Wrap your app in `SApp`
Wrap your root `app.vue` in ``, once:
```vue-html
```
This isn't optional - without it, Tooltip loses its shared hover-delay
behavior, and `useModal()`/`useDrawer()`/`useSlideover()`/`useConfirm()` (the "open from
anywhere" APIs) have nothing to render into. See [App](https://selarasui.my.id/components/layout/app)
for the full picture, including the two components (`SToast`,
`SCommandPalette`) that still need to be placed yourself even with `SApp`
already wrapping everything.
## Import the CSS
Selaras doesn't inject its own CSS for you - import it explicitly in your
own stylesheet, after Tailwind itself:
```css [assets/css/main.css]
@import "tailwindcss";
@import "@sewadah/selaras";
@import "#selaras/tailwind.css";
```
Then point Nuxt at that file:
```ts
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
css: ['~/assets/css/main.css'],
})
```
`@import "@sewadah/selaras";` brings in the default theme and component base
styles - see [Theming](https://selarasui.my.id/overview/theming/overview). `#selaras/tailwind.css` is generated
by the Nuxt module. Import it in this same entry so library class candidates
and adaptive presentation use your app's final Tailwind theme.
### Complete explicit theme
Applications with a complete existing semantic theme can import Selaras's
structural entry instead of its owned default foundations:
```css [assets/css/main.css]
@import "tailwindcss";
@import "@sewadah/selaras/structural.css";
@import "#selaras/tailwind.css";
```
This entry retains source discovery, variants, motion and semantic bindings.
Provide a light and dark recipe for every role your components can render,
including any built-in role you keep and each custom role you register. Provide
the functional `--selaras-*` inputs required by your design system as well.
It is not an unthemed zero-configuration mode: incomplete structural themes
have no Selaras fallback palette or runtime completeness diagnostic. Use the
aggregate import unless you provide that complete contract.
If your project also renders long-form markdown/CMS content, add
[prose.css](https://selarasui.my.id/components/typography/prose) the same way:
```css [assets/css/main.css]
@import "tailwindcss";
@import "@sewadah/selaras";
@import "#selaras/tailwind.css";
@import "@sewadah/selaras/prose.css";
```
Continue with [Component usage](https://selarasui.my.id/overview/getting-started/component-usage) and [Nuxt configuration](https://selarasui.my.id/overview/getting-started/nuxt-configuration).
# Introduction
Selaras is a UI component library for Nuxt.
It uses [Reka UI](https://reka-ui.com){rel=""nofollow""} for accessible UI primitives. It uses Tailwind CSS v4 for styling and [tailwind-variants](https://www.tailwind-variants.org){rel=""nofollow""} for component variants.
Selaras provides ready-to-use components for forms, navigation, data display, overlays, content, and application layouts.
## Design goals
### CSS-first theming
Selaras uses CSS custom properties for design values.
You can change colors, surfaces, text, borders, radius, shadows, and other theme values with CSS.
For example:
```css
:root {
--selaras-color-primary-fill: #6d4aff;
--selaras-surface-canvas: #f9f9f9;
--selaras-surface-default: #ffffff;
--selaras-radius-base: 0.5rem;
}
```
You do not need JavaScript or Nuxt configuration for normal theme changes.
Use Nuxt configuration only when Selaras needs build-time information. For example, use it when you add a new color role such as `tertiary`.
See [Theming](https://selarasui.my.id/overview/theming/overview) for all theme options.
### Predictable component customization
Each component has one `:ui` prop for instance-level customization.
Use `:ui` when you want to change the classes or attributes of one component.
```vue-html
Save
```
Use CSS theme tokens when you want to change design values across many components.
Use `app.config.ts` or `STheme` when you want to change component configuration across an application or a subtree.
### Accessible primitives
Selaras uses Reka UI for many interactive primitives.
Reka UI provides keyboard interaction, focus management, ARIA behavior, and other accessibility features for these primitives.
Selaras adds its own components and styling on top of this foundation.
### Adaptive interfaces
Some Selaras components can use a different presentation when the available screen space changes.
For example, a component can use a popover on a larger screen and a modal or drawer on a smaller screen.
The component keeps one public API while Selaras changes the presentation.
### CSS-based motion
Selaras uses CSS for component transitions and interaction states.
It does not require a JavaScript animation library for its standard motion.
Selaras also respects the user's reduced-motion preference.
## What you get
When you install Selaras, Nuxt automatically imports the components with an `S` prefix.
For example:
```vue-html
```
Selaras also provides composables for application-level UI:
```ts
useModal()
useDrawer()
useSlideover()
useToast()
useCommandPalette()
```
Selaras can also provide small DOM behaviors through directives, such as:
```vue-html
Save
```
## Where to start
Start with [Installation](https://selarasui.my.id/overview/getting-started/installation).
Then read [Theming](https://selarasui.my.id/overview/theming/overview) to learn how to customize Selaras with CSS.
Use the component documentation when you need the props, slots, events, variants, and `:ui` options for a specific component.
## Acknowledgments
Selaras uses [Reka UI](https://reka-ui.com){rel=""nofollow""} as its primitive foundation.
We appreciate the following projects for their contributions to the Vue ecosystem and the inspiration they provide for thoughtful component design:
- [Nuxt UI](https://ui.nuxt.com){rel=""nofollow""}
- [PrimeVue](https://primevue.org){rel=""nofollow""}
- [shadcn-vue](https://www.shadcn-vue.com){rel=""nofollow""}
- [UI Thing](https://ui-thing.behonbaker.com){rel=""nofollow""}
- [Quasar](https://quasar.dev){rel=""nofollow""}
Their work has helped shape our thinking about usability, customization, and developer experience as we develop Selaras.
# Messages and localization
Internal labels and messages come from a shared English registry. Override only the keys your application needs; the remaining keys keep their defaults.
## Global messages
```ts [app.config.ts]
export default defineAppConfig({
selaras: {
locale: 'id-ID',
messages: {
close: 'Tutup',
clear: 'Hapus',
search: 'Cari...',
noOptions: 'Tidak ada pilihan',
paginationInfo: (page: number, total: number) => `Halaman ${page} dari ${total}`,
},
},
})
```
Messages include accessible labels as well as visible text. Function-valued messages must remain functions with the documented arguments. See [useMessages](https://selarasui.my.id/utilities/composables/use-messages) for every key and its default.
For interpolated announcements such as Stepper progress, keep the message as a
function: `` stepperProgress: (step, total) => `Langkah ${step} dari ${total}` ``.
The [Stepper progress slot](https://selarasui.my.id/components/navigation/stepper#progress-announcements)
also allows an individual instance to supply its own text.
## Local content
```vue-html
Save changes
```
Text you supply through props, item labels, and slots stays under your control. Use each component's documented props or slots to customize individual instances. There is no generic per-component `messages` prop or scoped message registry on `STheme`.
## Formatting locale
`selaras.locale` sets the BCP 47 locale used by locale-aware formatting and date controls. It does not translate the message registry. Configure both when localizing an application. Selaras does not ship additional translation packs.
Use [useLocale](https://selarasui.my.id/utilities/composables/use-locale) and [useMessages](https://selarasui.my.id/utilities/composables/use-messages) in your own components to share the application's configuration.
# Migrating to 1.0
Selaras 1.0 makes the theme and component contracts explicit. Most applications
can keep the usual installation and component usage. Review this guide if you
customize CSS variables, define a theme, use scoped configuration, or use an
adaptive picker.
## 1. Keep the aggregate CSS import unless you own a complete theme
The normal import remains the recommended starting point:
```css [assets/css/main.css]
@import "tailwindcss";
@import "@sewadah/selaras";
@import "#selaras/tailwind.css";
```
It includes Selaras's default foundations and semantic recipes. If your design
system supplies every role and functional token that Selaras can render, use
the structural entry instead:
```css [assets/css/main.css]
@import "tailwindcss";
@import "@sewadah/selaras/structural.css";
@import "#selaras/tailwind.css";
```
Structural CSS is an explicit complete-theme contract. It does not fall back
to Selaras palette values or diagnose omitted roles at runtime. See
[Theming](https://selarasui.my.id/overview/theming/overview) for the required semantic recipes and functional
inputs.
## 2. Use the public token namespaces
`--selaras-*` variables are customization inputs. Use
`--selaras-resolved-*` only when your own CSS needs the effective value after
Selaras has applied defaults, modes, and scopes. The old `--ui-*` aliases are
not part of the 1.0 contract; move overrides to the semantic input that
matches their purpose.
```css
/* Before: a legacy, collision-prone implementation alias. */
:root { --ui-border: oklch(0.8 0.02 260); }
/* After: the public semantic input. */
:root { --selaras-border-default: oklch(0.8 0.02 260); }
```
For a color role, provide individual recipe leaves such as
`--selaras-color-primary-fill`. Do not target generated selectors,
`--_selaras-*` variables, or mode/portal attributes: they are implementation
details and can change without a public migration path.
## 3. Register colors at Nuxt build time
A custom `color` name must be registered in `nuxt.config.ts`. Registration
generates its CSS bindings and augments `ColorRole`, so the same role works
across components without component-specific configuration:
```ts [nuxt.config.ts]
import { defineColor } from '@sewadah/selaras/theme'
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
selaras: {
theme: {
colors: {
tertiary: defineColor({
light: {
fill: 'var(--brand-tertiary)',
onFill: '#fff',
subtle: 'var(--brand-tertiary-subtle)',
onSubtle: '#172033',
text: '#284772',
border: '#7aa1d6',
},
dark: {
fill: '#91bcf5',
onFill: '#10213b',
subtle: '#18385f',
onSubtle: '#e9f2ff',
text: '#b7d4ff',
border: '#6b9bd8',
},
}),
},
},
},
})
```
Then use ``, ``, and
other applicable components normally. For a single opaque sRGB brand color,
`defineColorFromSeed()` can create an accessible complete recipe. Existing
token systems can map resolved CSS values into `defineColor()`; the DTCG helper
converts resolved color values, but does not import or resolve a whole DTCG
document.
## 4. Move runtime settings under `selaras`
Place application-level Selaras configuration under `app.config.selaras`.
This prevents collisions with other Nuxt modules and gives scoped `STheme`
overrides the same vocabulary:
```ts [app.config.ts]
export default defineAppConfig({
selaras: {
ui: {
button: { slots: { base: 'rounded-full' } },
},
defaults: {
button: { size: 'lg' },
},
tokens: {
light: { surfaces: { default: '#f8fafc' } },
},
},
})
```
Use `STheme`'s `defaults` prop for scoped component prop defaults. It supports
only components that document default support; `ui` recipe overrides remain
available for every typed recipe key.
## 5. Replace `mobileModal` with `adaptive`
Select, Autocomplete, DatePicker, and ColorPicker now opt into small-screen
presentation with `adaptive`:
```vue-html
```
The presentation is selected when the control opens and remains stable until it
closes. Autocomplete places its input and suggestions inside the mobile modal. The module reads the Tailwind breakpoint selected by
`selaras.adaptive.breakpoint`; see [Installation](https://selarasui.my.id/overview/getting-started/nuxt-configuration#adaptive-breakpoint).
## 6. Follow controlled and native-control conventions
Use `v-model` for controlled values and `default-value` for an uncontrolled
initial value. Controls with open state use `v-model:open` and `default-open`
the same way. Do not pass both forms for the same state.
```vue-html
```
For native form behavior, put `name`, `form`, `required`, `readonly`, ARIA
attributes, and focus listeners on the Selaras control itself. Select and
picker controls synchronize their native form value and reset behavior; a
multiple Select rejects duplicate selected identities.
## 7. Use supported imports
Import from the package root, documented secondary entries, or
`@sewadah/selaras/components/*`. Do not import renderer queues, internal
composables, raw recipe files, or generated implementation files from `dist`.
Those paths were never stable integration points and are intentionally outside
the 1.0 public API.
Read the [Installation](https://selarasui.my.id/overview/getting-started/installation), [Theming](https://selarasui.my.id/overview/theming/overview),
and individual component pages for the resulting API reference. The generated
changelog remains a historical record; this page is the migration contract.
# Nuxt configuration
## Options
Configure the module under the `selaras` key:
```ts
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
selaras: {
// Prefix used for auto-imported components.
prefix: 'S', // default
},
})
```
| Option | Type | Default | Description |
| --------------------- | ------------------------------------------------- | -------------- | --------------------------------------------------------------------- |
| `prefix` | `string` | `'S'` | Prefix used for auto-imported components (`SButton`, `SModal`, ...). |
| `classPrefix` | `string` | none | Namespaces every class Selaras's own components render - see below. |
| `theme.colors` | `Record>` | built-in roles | Build-time light and dark color recipes; also registers custom roles. |
| `adaptive.breakpoint` | `string` | `'md'` | Selects a Tailwind `--breakpoint-*` token for adaptive presentation. |
## Adaptive breakpoint
CSS owns breakpoint values. Override the default condition in your stylesheet:
```css
@theme {
--breakpoint-md: 60rem;
}
```
Or define your own `--breakpoint-tablet` token and select it with
`selaras: { adaptive: { breakpoint: 'tablet' } }`. Supported values are simple
lengths in `px`, `rem` or `em`. Keep breakpoint units consistent with your other
Tailwind conditions. Selaras reads the resolved length after mount and uses the
native `(width < …)` media query, including under a class prefix. Its initial
SSR render uses desktop presentation; the existing adaptive controls hold their
chosen presentation until they close.
These are build-time `@theme` conditions. Changing a breakpoint CSS variable at
runtime does not rewrite Tailwind's compiled responsive rules. A missing or
unsupported binding produces a console diagnostic and disables mobile
presentation; there is no implicit 768px fallback.
## Class prefix
Tailwind v4 supports namespacing every one of its own utilities behind a
prefix (`@import "tailwindcss" prefix(tw);`, which turns `px-4` into
`tw:px-4`) - typically to avoid collisions with another CSS framework or a
second, differently-configured Tailwind build in the same project. Selaras
can match that prefix in the classes its own components render, via
`classPrefix` under the `selaras` key in `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
selaras: {
classPrefix: 'tw',
},
})
```
This is a module option, not an `app.config.ts` value - Tailwind CSS
generation happens at build time, so Selaras needs to know the prefix
early enough to generate the class candidates imported through
`#selaras/tailwind.css`. Your own Tailwind entry produces the matching CSS
(Tailwind never generates CSS for a class it can't find as literal text
anywhere, whether that's your own templates or a safelist - an
`app.config.ts` value alone, read only at render time, could never
produce that).
**This value must match your own Tailwind prefix exactly.** Selaras has
no way to detect your Tailwind configuration, so if the two ever drift -
`classPrefix: 'tw'` here while your CSS still says `prefix(ui)`, or one
set and not the other - every component renders classes your Tailwind
build never generated CSS for, and the entire UI comes out unstyled. No
error is raised either way; it silently looks like a blank stylesheet.
Only set this if you've already added `prefix(...)` to your own
`@import "tailwindcss"`.
**Your own classes need the same prefix, written by hand.** Selaras's
automatic handling only covers classes baked into its own shipped
components - it has no way to see a string you write yourself, so a
`:ui` override (or your own custom component built the same way, on
`resolveSlot`/`tv()`) needs the prefix written directly into the string:
```vue-html
```
This isn't a Selaras-specific gotcha - it's the same rule Tailwind's
prefix feature already applies to every other class in your project once
you've adopted it (a plain `
` needs the same
literal prefix). See [Theming](https://selarasui.my.id/overview/theming/overview) for the other ways to
customize a component.
## Custom color roles
`selaras.theme.colors` registers build-time color recipes, including custom role names. Each role supplies light and dark recipes. See [Color roles](https://selarasui.my.id/overview/theming/color-roles) for the full contract. Runtime classes, defaults, messages, and icons belong in `app.config.ts`, not these module options.
See [Global configuration](https://selarasui.my.id/overview/theming/global-configuration) and [Color modes](https://selarasui.my.id/overview/theming/color-modes).
# TypeScript
Every component exports named, documented `Props`/`Emits`/`Slots`
interfaces (`ButtonProps`, `ModalEmits`, `PopoverSlots`, ...) - useful when
you're wrapping a component, forwarding its props from your own, or just
want autocomplete on an object you're building up before passing it down.
## From the `@sewadah/selaras/types` barrel
The simplest path - one import for every component's types:
```ts
import type { ModalEmits, ModalProps, PopoverSlots } from '@sewadah/selaras/types'
```
## From a specific component
Each component's `.vue` file is also a valid type-only import path, if you
only need one component and don't want the rest pulled into scope:
```ts
import type { PopoverProps } from '@sewadah/selaras/components/Popover.vue'
```
## Module options
The module's own configuration type (the shape of the `selaras` key in
`nuxt.config.ts`, see [Installation](https://selarasui.my.id/overview/getting-started/installation)) is exported
from the package root:
```ts
import type { ModuleOptions } from '@sewadah/selaras'
```
# Color modes
Selaras automatically installs Nuxt Color Mode and ships light and dark token recipes. The `system` preference follows the operating system; the resolved appearance is either `light` or `dark`.
## Built-in controls
```vue-html
```
[ColorModeToggle](https://selarasui.my.id/components/layout/color-mode-toggle) switches appearance through a button. [ColorModeSwitch](https://selarasui.my.id/components/layout/color-mode-switch) offers a switch. Both use the shared color-mode state.
## Preference and resolved appearance
```vue
Use system appearance
```
Read `colorMode.preference` for the user's choice and `colorMode.value` for the resolved mode. Nuxt Color Mode manages the root `.light` and `.dark` classes and persistence. Configure its `colorMode` options in `nuxt.config.ts` when you need a different initial preference or storage policy.
## Scoped appearance
```vue-html
Dark content inside a light page
```
`STheme` accepts `inherit`, `light`, and `dark`. An explicit scoped mode needs a DOM owner through `as`. See [Scoped themes](https://selarasui.my.id/overview/theming/scoped-themes) for component configuration and token overrides.
Color modes choose light or dark recipes. Semantic roles such as `primary` and `danger` select a color purpose; documentation theme presets choose a visual design. These are separate settings.
# Color roles
### Built-in component color roles
Selaras supplies these roles:
- `primary`
- `secondary`
- `success`
- `info`
- `warning`
- `danger`
- `neutral`
The default theme maps these roles to foundation palettes:
| Role | Default foundation |
| ----------- | ------------------ |
| `primary` | `indigo` |
| `secondary` | `plum` |
| `success` | `green` |
| `info` | `blue` |
| `warning` | `yellow` |
| `danger` | `red` |
| `neutral` | `gray` |
The role name is the meaning. The foundation name is the hue family.
For example, the default `warning` recipe reads the Selaras yellow palette.
The default `danger` recipe reads the Selaras red palette.
### Change one exact role value
You can override a public role input directly:
```css
:root {
--selaras-color-warning-fill: #ffff00;
--selaras-color-warning-on-fill: #000000;
}
```
Selaras uses the exact CSS value that you provide. It does not change it.
This example changes only two leaves. The other warning leaves still use their
default values. If you want one consistent custom warning theme, define the
complete recipe.
### Build one role from one CSS color
You do not need a TypeScript helper when you want to start from one CSS color.
You can keep the base color exact and derive the other values with CSS:
```css
:root {
--brand: #fd5e53;
--selaras-color-primary-fill:
var(--brand);
--selaras-color-primary-fill-hover:
color-mix(in oklch, var(--brand), black 8%);
--selaras-color-primary-fill-pressed:
color-mix(in oklch, var(--brand), black 15%);
--selaras-color-primary-on-fill:
white;
--selaras-color-primary-indicator:
color-mix(in oklch, var(--brand), black 18%);
--selaras-color-primary-subtle:
color-mix(in oklch, var(--brand) 12%, transparent);
--selaras-color-primary-subtle-hover:
color-mix(in oklch, var(--brand) 18%, transparent);
--selaras-color-primary-subtle-pressed:
color-mix(in oklch, var(--brand) 24%, transparent);
--selaras-color-primary-on-subtle:
color-mix(in oklch, var(--brand), black 35%);
--selaras-color-primary-text:
color-mix(in oklch, var(--brand), black 20%);
--selaras-color-primary-text-hover:
color-mix(in oklch, var(--brand), black 28%);
--selaras-color-primary-text-pressed:
color-mix(in oklch, var(--brand), black 35%);
--selaras-color-primary-border:
var(--brand);
--selaras-color-primary-focus:
var(--brand);
}
```
This is only one possible recipe. You own the color choices.
Test the result in the components and contexts that your application uses.
Selaras does not change CSS values that you author.
### Complete role recipe
A color role has these public inputs:
```text
fill
fill-hover
fill-pressed
on-fill
indicator
subtle
subtle-hover
subtle-pressed
on-subtle
text
text-hover
text-pressed
border
focus
```
For `warning`, the full CSS names are:
```css
:root {
--selaras-color-warning-fill: ...;
--selaras-color-warning-fill-hover: ...;
--selaras-color-warning-fill-pressed: ...;
--selaras-color-warning-on-fill: ...;
--selaras-color-warning-indicator: ...;
--selaras-color-warning-subtle: ...;
--selaras-color-warning-subtle-hover: ...;
--selaras-color-warning-subtle-pressed: ...;
--selaras-color-warning-on-subtle: ...;
--selaras-color-warning-text: ...;
--selaras-color-warning-text-hover: ...;
--selaras-color-warning-text-pressed: ...;
--selaras-color-warning-border: ...;
--selaras-color-warning-focus: ...;
}
```
Replace `warning` with any registered role name.
The component variant selects the leaves that it needs. For example, a solid
Button uses `fill`, `on-fill`, `fill-hover`, and `fill-pressed`. A soft Button
uses the `subtle` leaves. Essential unpaired graphics, such as progress and
radio indicators, use `indicator`. When it is omitted, it follows `border`.
You can also supply different values for dark mode:
```css
:root {
--selaras-color-warning-fill: #ffff00;
--selaras-color-warning-on-fill: #000000;
}
:root.dark {
--selaras-color-warning-fill: #eaea00;
--selaras-color-warning-on-fill: #000000;
}
```
## Nuxt configuration and theme helpers
Use CSS first.
Use `nuxt.config.ts` when Selaras needs build-time information. The most common
color case is a new role name.
### Add a new role
Built-in role names already work without theme configuration.
A new role such as `tertiary` or `premium` must be registered at build time so
Selaras can:
- generate its role bindings
- add it to `ColorRole`
- make `color="tertiary"` type-safe
The current Nuxt API requires a light and dark recipe:
```ts
import { defineColor } from '@sewadah/selaras/theme'
export default defineNuxtConfig({
modules: ['@sewadah/selaras'],
selaras: {
theme: {
colors: {
tertiary: defineColor({
light: {
fill: 'var(--company-tertiary-fill)',
onFill: 'var(--company-tertiary-on-fill)',
subtle: 'var(--company-tertiary-subtle)',
onSubtle: 'var(--company-tertiary-on-subtle)',
text: 'var(--company-tertiary-text)',
border: 'var(--company-tertiary-border)',
},
dark: {
fill: 'var(--company-tertiary-fill-dark)',
onFill: 'var(--company-tertiary-on-fill-dark)',
subtle: 'var(--company-tertiary-subtle-dark)',
onSubtle: 'var(--company-tertiary-on-subtle-dark)',
text: 'var(--company-tertiary-text-dark)',
border: 'var(--company-tertiary-border-dark)',
},
}),
},
},
},
})
```
After registration, you can use:
```vue-html
Tertiary
```
You can still override the registered role from CSS:
```css
:root {
--selaras-color-tertiary-fill: #8b5cf6;
}
```
The Nuxt configuration registers the role. CSS can still own its visual values.
### Override a built-in recipe in Nuxt config
You can also replace a built-in recipe:
```ts
import { defineColor } from '@sewadah/selaras/theme'
export default defineNuxtConfig({
selaras: {
theme: {
colors: {
warning: defineColor({
light: {
fill: '#ffff00',
onFill: '#000000',
subtle: '#fffbd1',
onSubtle: '#3d3d00',
text: '#6b6b00',
border: '#a3a300',
},
dark: {
fill: '#eaea00',
onFill: '#000000',
subtle: '#2a2a00',
onSubtle: '#ffff99',
text: '#ffff66',
border: '#baba00',
},
}),
},
},
},
})
```
`defineColor()` validates the required recipe shape. It does not change the
values that you provide.
The required leaves in each mode are:
- `fill`
- `onFill`
- `subtle`
- `onSubtle`
- `text`
- `border`
These leaves are optional:
- `fillHover`
- `fillPressed`
- `indicator` (defaults to `border`)
- `subtleHover`
- `subtlePressed`
- `textHover`
- `textPressed`
- `focus`
If an optional interaction leaf is missing, Selaras links it to the nearest
defined state. It does not calculate a new hue for `defineColor()`.
### Optional helper: `defineColorFromSeed()`
`defineColorFromSeed()` is not the CSS-first theme path.
Use it when you want Selaras to preserve one opaque sRGB color as the resting
solid fill and derive the rest of the recipe around it.
```ts
import { defineColorFromSeed } from '@sewadah/selaras/theme'
export default defineNuxtConfig({
selaras: {
theme: {
colors: {
coral: defineColorFromSeed('#FD5E53'),
},
},
},
})
```
The light and dark resting `fill` values remain exactly the six-digit color
that you pass. Selaras chooses black or white `onFill`, derives distinct
interaction states, and derives text, border, focus, and indicator colors
against the supplied surfaces. If those supporting families cannot be derived
safely, the helper rejects the input and directs you to `defineColor()`.
Use CSS or `defineColor()` when you need exact control over every state.
The helper currently accepts only opaque six-digit sRGB hex values. It cannot
evaluate CSS variables, transparent colors, or arbitrary CSS expressions.
If you use custom surfaces, you can give the resolved light and dark surface
colors to the helper:
```ts
defineColorFromSeed('#FD5E53', {
surfaces: {
light: '#f4f0e8',
dark: '#20242a',
},
})
```
### Optional helper: DTCG resolved colors
If your design-token pipeline uses DTCG, resolve aliases and modes in that
pipeline first.
`dtcgColorToCss()` converts a resolved structured DTCG color value to CSS. It
does not parse a token document. It does not resolve aliases. It does not
select a mode.
```ts
import { dtcgColorToCss } from '@sewadah/selaras/theme'
const fill = dtcgColorToCss(tokens.brand.fill.$value, {
path: 'semantic.brand.fill.$value',
})
```
You can then use the result in `defineColor()` or in your own generated CSS.
# Complete themes
### Complete CSS theme example
The following example replaces the main Selaras visual theme with CSS only.
It defines:
- surfaces
- general text
- general borders
- scrim
- radius
- shadows
- `primary`
- `secondary`
- `success`
- `info`
- `warning`
- `danger`
- `neutral`
It also defines separate light and dark values. When adapting this example,
check visible borders against the surfaces they touch in each mode. Surface
and border tokens are independent; see
[Check borders against their surfaces](https://selarasui.my.id/overview/theming/css-tokens#check-borders-against-their-surfaces).
These values are examples. Use the values that match your product.
```css
/* Light mode */
:root {
/* Surfaces */
--selaras-surface-canvas: #f9f9f9;
--selaras-surface-default: #fffdf9;
--selaras-surface-elevated: #ffffff;
--selaras-surface-inverted: #161616;
/* General text */
--selaras-text-default: #1f1f1f;
--selaras-text-muted: #707070;
--selaras-text-inverted: #ffffff;
/* General borders */
--selaras-border-default: #e7e2dc;
--selaras-border-muted: #f2eee9;
--selaras-border-hover: #d4cdc5;
/* Overlay */
--selaras-scrim: rgb(0 0 0 / 0.55);
/* Radius */
--selaras-radius-base: 0.5rem;
--selaras-radius-sm: 0.375rem;
--selaras-radius-md: 0.5rem;
--selaras-radius-lg: 1rem;
--selaras-radius-full: 9999px;
/* Shadows */
--selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
--selaras-shadow-md: 0 6px 20px rgb(0 0 0 / 0.10);
--selaras-shadow-lg: 0 18px 48px rgb(0 0 0 / 0.16);
/* Primary */
--selaras-color-primary-fill: #6d4aff;
--selaras-color-primary-fill-hover: #5e3bf0;
--selaras-color-primary-fill-pressed: #4f2fd8;
--selaras-color-primary-on-fill: #ffffff;
--selaras-color-primary-subtle: #f0ecff;
--selaras-color-primary-subtle-hover: #e7e0ff;
--selaras-color-primary-subtle-pressed: #ddd3ff;
--selaras-color-primary-on-subtle: #3d267f;
--selaras-color-primary-text: #5b3be0;
--selaras-color-primary-text-hover: #4f31c4;
--selaras-color-primary-text-pressed: #4328a9;
--selaras-color-primary-border: #8d73ff;
--selaras-color-primary-focus: #6d4aff;
/* Secondary */
--selaras-color-secondary-fill: #d946ef;
--selaras-color-secondary-fill-hover: #c735dc;
--selaras-color-secondary-fill-pressed: #ad27c2;
--selaras-color-secondary-on-fill: #ffffff;
--selaras-color-secondary-subtle: #fcecff;
--selaras-color-secondary-subtle-hover: #f8ddff;
--selaras-color-secondary-subtle-pressed: #f3ccff;
--selaras-color-secondary-on-subtle: #70207c;
--selaras-color-secondary-text: #b72cca;
--selaras-color-secondary-text-hover: #9822aa;
--selaras-color-secondary-text-pressed: #7d1b8b;
--selaras-color-secondary-border: #e879f9;
--selaras-color-secondary-focus: #d946ef;
/* Success */
--selaras-color-success-fill: #16a34a;
--selaras-color-success-fill-hover: #138a3f;
--selaras-color-success-fill-pressed: #107436;
--selaras-color-success-on-fill: #ffffff;
--selaras-color-success-subtle: #eaf8ef;
--selaras-color-success-subtle-hover: #dcf3e4;
--selaras-color-success-subtle-pressed: #ccecd8;
--selaras-color-success-on-subtle: #14532d;
--selaras-color-success-text: #16803f;
--selaras-color-success-text-hover: #126b35;
--selaras-color-success-text-pressed: #0f592d;
--selaras-color-success-border: #3fc16d;
--selaras-color-success-focus: #16a34a;
/* Info */
--selaras-color-info-fill: #0ea5e9;
--selaras-color-info-fill-hover: #0c91cc;
--selaras-color-info-fill-pressed: #0a7db0;
--selaras-color-info-on-fill: #ffffff;
--selaras-color-info-subtle: #e9f7fe;
--selaras-color-info-subtle-hover: #d8f1fd;
--selaras-color-info-subtle-pressed: #c7eafb;
--selaras-color-info-on-subtle: #0c4a6e;
--selaras-color-info-text: #087fb6;
--selaras-color-info-text-hover: #076b9a;
--selaras-color-info-text-pressed: #065a82;
--selaras-color-info-border: #38bdf8;
--selaras-color-info-focus: #0ea5e9;
/* Warning */
--selaras-color-warning-fill: #ffff00;
--selaras-color-warning-fill-hover: #eeee00;
--selaras-color-warning-fill-pressed: #d8d800;
--selaras-color-warning-on-fill: #111111;
--selaras-color-warning-subtle: #fffed6;
--selaras-color-warning-subtle-hover: #fffca8;
--selaras-color-warning-subtle-pressed: #fff77a;
--selaras-color-warning-on-subtle: #575700;
--selaras-color-warning-text: #757500;
--selaras-color-warning-text-hover: #626200;
--selaras-color-warning-text-pressed: #505000;
--selaras-color-warning-border: #bdbd00;
--selaras-color-warning-focus: #8a8a00;
/* Danger */
--selaras-color-danger-fill: #ff3b5c;
--selaras-color-danger-fill-hover: #ed2e4e;
--selaras-color-danger-fill-pressed: #d62241;
--selaras-color-danger-on-fill: #ffffff;
--selaras-color-danger-subtle: #fff0f3;
--selaras-color-danger-subtle-hover: #ffe1e7;
--selaras-color-danger-subtle-pressed: #ffd0da;
--selaras-color-danger-on-subtle: #8a1c32;
--selaras-color-danger-text: #d92c49;
--selaras-color-danger-text-hover: #bd243f;
--selaras-color-danger-text-pressed: #a21d35;
--selaras-color-danger-border: #ff6b84;
--selaras-color-danger-focus: #ff3b5c;
/* Neutral */
--selaras-color-neutral-fill: #27272a;
--selaras-color-neutral-fill-hover: #3f3f46;
--selaras-color-neutral-fill-pressed: #52525b;
--selaras-color-neutral-on-fill: #ffffff;
--selaras-color-neutral-subtle: #f4f4f5;
--selaras-color-neutral-subtle-hover: #e4e4e7;
--selaras-color-neutral-subtle-pressed: #d4d4d8;
--selaras-color-neutral-on-subtle: #27272a;
--selaras-color-neutral-text: #3f3f46;
--selaras-color-neutral-text-hover: #27272a;
--selaras-color-neutral-text-pressed: #18181b;
--selaras-color-neutral-border: #a1a1aa;
--selaras-color-neutral-focus: #52525b;
}
/* Dark mode */
:root.dark {
/* Surfaces */
--selaras-surface-canvas: #0c0c0d;
--selaras-surface-default: #0f0f12;
--selaras-surface-elevated: #18181d;
--selaras-surface-inverted: #f6f6f6;
/* General text */
--selaras-text-default: #f4f4f5;
--selaras-text-muted: #a1a1aa;
--selaras-text-inverted: #18181b;
/* General borders */
--selaras-border-default: #2d2d35;
--selaras-border-muted: #232329;
--selaras-border-hover: #3b3b45;
/* Overlay */
--selaras-scrim: rgb(0 0 0 / 0.72);
/* Radius */
--selaras-radius-base: 0.5rem;
--selaras-radius-sm: 0.375rem;
--selaras-radius-md: 0.5rem;
--selaras-radius-lg: 1rem;
--selaras-radius-full: 9999px;
/* Shadows */
--selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.28);
--selaras-shadow-md: 0 8px 28px rgb(0 0 0 / 0.38);
--selaras-shadow-lg: 0 22px 56px rgb(0 0 0 / 0.48);
/* Primary */
--selaras-color-primary-fill: #8b6cff;
--selaras-color-primary-fill-hover: #9c82ff;
--selaras-color-primary-fill-pressed: #ad98ff;
--selaras-color-primary-on-fill: #171020;
--selaras-color-primary-subtle: #261f46;
--selaras-color-primary-subtle-hover: #302858;
--selaras-color-primary-subtle-pressed: #3a306a;
--selaras-color-primary-on-subtle: #d9d0ff;
--selaras-color-primary-text: #b9a9ff;
--selaras-color-primary-text-hover: #c9bcff;
--selaras-color-primary-text-pressed: #d8d0ff;
--selaras-color-primary-border: #7259dc;
--selaras-color-primary-focus: #9f8aff;
/* Secondary */
--selaras-color-secondary-fill: #e879f9;
--selaras-color-secondary-fill-hover: #ef91fb;
--selaras-color-secondary-fill-pressed: #f3a9fc;
--selaras-color-secondary-on-fill: #2a0b2e;
--selaras-color-secondary-subtle: #3c173f;
--selaras-color-secondary-subtle-hover: #4a1c4e;
--selaras-color-secondary-subtle-pressed: #59235d;
--selaras-color-secondary-on-subtle: #f7c9ff;
--selaras-color-secondary-text: #f0a5fb;
--selaras-color-secondary-text-hover: #f5bafd;
--selaras-color-secondary-text-pressed: #f9d0fe;
--selaras-color-secondary-border: #c65fd6;
--selaras-color-secondary-focus: #e879f9;
/* Success */
--selaras-color-success-fill: #22c55e;
--selaras-color-success-fill-hover: #35d36f;
--selaras-color-success-fill-pressed: #4ade80;
--selaras-color-success-on-fill: #061a0c;
--selaras-color-success-subtle: #12331f;
--selaras-color-success-subtle-hover: #174129;
--selaras-color-success-subtle-pressed: #1d5033;
--selaras-color-success-on-subtle: #bbf7d0;
--selaras-color-success-text: #86efac;
--selaras-color-success-text-hover: #a7f3c0;
--selaras-color-success-text-pressed: #c6f6d5;
--selaras-color-success-border: #3fba68;
--selaras-color-success-focus: #4ade80;
/* Info */
--selaras-color-info-fill: #38bdf8;
--selaras-color-info-fill-hover: #55c7fa;
--selaras-color-info-fill-pressed: #73d2fb;
--selaras-color-info-on-fill: #06151d;
--selaras-color-info-subtle: #112f3f;
--selaras-color-info-subtle-hover: #163c50;
--selaras-color-info-subtle-pressed: #1b4a61;
--selaras-color-info-on-subtle: #c7efff;
--selaras-color-info-text: #7dd3fc;
--selaras-color-info-text-hover: #9bdffc;
--selaras-color-info-text-pressed: #bae9fd;
--selaras-color-info-border: #2e9fd0;
--selaras-color-info-focus: #38bdf8;
/* Warning */
--selaras-color-warning-fill: #ffff33;
--selaras-color-warning-fill-hover: #ffff66;
--selaras-color-warning-fill-pressed: #ffff88;
--selaras-color-warning-on-fill: #111100;
--selaras-color-warning-subtle: #333300;
--selaras-color-warning-subtle-hover: #414100;
--selaras-color-warning-subtle-pressed: #505000;
--selaras-color-warning-on-subtle: #ffffb8;
--selaras-color-warning-text: #ffff66;
--selaras-color-warning-text-hover: #ffff88;
--selaras-color-warning-text-pressed: #ffffaa;
--selaras-color-warning-border: #baba24;
--selaras-color-warning-focus: #ffff66;
/* Danger */
--selaras-color-danger-fill: #ff5c78;
--selaras-color-danger-fill-hover: #ff748c;
--selaras-color-danger-fill-pressed: #ff8ca0;
--selaras-color-danger-on-fill: #26070d;
--selaras-color-danger-subtle: #3c1720;
--selaras-color-danger-subtle-hover: #4b1c28;
--selaras-color-danger-subtle-pressed: #5b2330;
--selaras-color-danger-on-subtle: #ffd0da;
--selaras-color-danger-text: #ff9caf;
--selaras-color-danger-text-hover: #ffb3c1;
--selaras-color-danger-text-pressed: #ffc8d2;
--selaras-color-danger-border: #d64f68;
--selaras-color-danger-focus: #ff6b84;
/* Neutral */
--selaras-color-neutral-fill: #e4e4e7;
--selaras-color-neutral-fill-hover: #f4f4f5;
--selaras-color-neutral-fill-pressed: #ffffff;
--selaras-color-neutral-on-fill: #18181b;
--selaras-color-neutral-subtle: #27272a;
--selaras-color-neutral-subtle-hover: #3f3f46;
--selaras-color-neutral-subtle-pressed: #52525b;
--selaras-color-neutral-on-subtle: #f4f4f5;
--selaras-color-neutral-text: #d4d4d8;
--selaras-color-neutral-text-hover: #e4e4e7;
--selaras-color-neutral-text-pressed: #f4f4f5;
--selaras-color-neutral-border: #71717a;
--selaras-color-neutral-focus: #a1a1aa;
}
```
This example changes the complete semantic theme without `nuxt.config.ts`.
Use `nuxt.config.ts` only when you need build-time features, such as a new
role name that must work with a component `color` prop.
# Component styling
## The `:ui` prop and `STheme`
Color tokens and component class overrides are separate systems.
Use theme tokens for design values. Use `:ui` when you want to change classes
or attributes for one component instance.
```vue-html
Custom
```
Each `:ui` key is a component slot.
A string adds classes. Selaras merges conflicting Tailwind classes with
`tailwind-merge`.
An object can contain `class` and other Vue attributes:
```vue-html
Save
```
## Precedence
For component classes, the order from low priority to high priority is:
1. component base `tv()` theme
2. `app.config.selaras.ui`
3. ancestor `STheme` recipes, from outer to inner
4. the component root `class`
5. the component `:ui` slot class
For design tokens, normal CSS cascade rules apply to the public
`--selaras-*` inputs. Managed `STheme` and runtime token scopes create explicit
theme owners for their subtree and supported portals.
# CSS tokens
### Start with CSS
For most theme changes, edit CSS only.
Selaras has public input variables. The main groups are:
| Input | Purpose |
| ------------------------------- | ----------------------------------------------------- |
| `--selaras-surface-*` | Page and surface colors |
| `--selaras-text-*` | General text colors |
| `--selaras-border-*` | General border colors |
| `--selaras-color--` | Component color roles, such as `primary` or `warning` |
| `--selaras-radius-*` | Corner radius |
| `--selaras-shadow-*` | Shadow values |
| `--selaras-z-*` | Overlay stacking |
| `--selaras-scrim` | Overlay backdrop |
Selaras also has `--selaras-resolved-*` variables. These are effective read
values. Do not use them as theme inputs.
### Small CSS changes
You can change one value:
```css
:root {
--selaras-radius-base: 0.375rem;
}
```
Or you can change general surfaces and text:
The light canvas is white by default. A card-heavy dashboard can use a subtle
canvas such as `#f9f9f9` while keeping its component surfaces white:
```css
:root {
--selaras-surface-canvas: #f9f9f9;
--selaras-surface-default: #ffffff;
--selaras-surface-elevated: #fafafa;
--selaras-text-default: #18181b;
--selaras-text-muted: #71717a;
--selaras-border-default: #e4e4e7;
--selaras-border-hover: #d4d4d8;
}
```
For dark mode, override the same inputs under `:root.dark`:
```css
:root.dark {
--selaras-surface-canvas: #0c0c0d;
--selaras-surface-default: #09090b;
--selaras-surface-elevated: #18181b;
--selaras-text-default: #fafafa;
--selaras-text-muted: #a1a1aa;
--selaras-border-default: #27272a;
--selaras-border-hover: #3f3f46;
}
```
Selaras follows the `.dark` class on `` for the document theme.
### Check borders against their surfaces
`--selaras-surface-*` and `--selaras-border-*` are independent inputs. Component
themes choose which ones to pair. For example,
[DashboardSidebar](https://selarasui.my.id/composites/dashboard/dashboard-sidebar) paints its background
with `surface-elevated` and draws its footer divider with `border-default`.
If both resolve to the same color, the divider blends into that background.
When a border should be visible, give it enough contrast against the surfaces
it touches. Check the actual component in both light and dark modes, including
hover and focus states, after changing either token or its foundation palette.
Different token names or different scale steps alone do not guarantee a visible
boundary. An intentionally blended border is also a valid design choice; each
border does not need to differ from every surface in your theme.
Selaras does not automatically adjust border colors or validate their contrast
when you override CSS variables. For transparent components, check the actual
background showing through, which may be your application's canvas.
### Change a complete foundation palette
The default foundations are Tailwind v4 theme variables.
For example, the default warning role uses the Selaras yellow foundation. You
can replace that foundation and keep the built-in warning recipe:
```css
@theme {
--color-selaras-yellow-50: var(--color-orange-50);
--color-selaras-yellow-100: var(--color-orange-100);
--color-selaras-yellow-200: var(--color-orange-200);
--color-selaras-yellow-300: var(--color-orange-300);
--color-selaras-yellow-400: var(--color-orange-400);
--color-selaras-yellow-500: var(--color-orange-500);
--color-selaras-yellow-600: var(--color-orange-600);
--color-selaras-yellow-700: var(--color-orange-700);
--color-selaras-yellow-800: var(--color-orange-800);
--color-selaras-yellow-900: var(--color-orange-900);
--color-selaras-yellow-950: var(--color-orange-950);
}
```
The warning role now uses your replacement foundation through its existing
recipe.
The same rule applies to:
- `blue` for the default `info` role
- `red` for the default `danger` role
- `gray` for the default `neutral` role
- `green` for the default `success` role
- `yellow` for the default `warning` role
- `indigo` for the default `primary` role
- `plum` for the default `secondary` role
The Selaras gray foundation also supplies the default general surfaces, text,
and borders. A change to the gray foundation can therefore change more than the
`neutral` component role.
### Map an existing design system
You do not have to rename your design tokens.
For example, your design system can use a `10` to `100` scale:
```css
:root {
--company-brand-10: ...;
--company-brand-20: ...;
--company-brand-30: ...;
--company-brand-40: ...;
--company-brand-50: ...;
--company-brand-60: ...;
--company-brand-70: ...;
--company-brand-80: ...;
--company-brand-90: ...;
--company-brand-100: ...;
--selaras-color-primary-fill: var(--company-brand-60);
--selaras-color-primary-fill-hover: var(--company-brand-70);
--selaras-color-primary-fill-pressed: var(--company-brand-80);
--selaras-color-primary-on-fill: var(--company-brand-10);
--selaras-color-primary-subtle: var(--company-brand-10);
--selaras-color-primary-subtle-hover: var(--company-brand-20);
--selaras-color-primary-subtle-pressed: var(--company-brand-30);
--selaras-color-primary-on-subtle: var(--company-brand-90);
--selaras-color-primary-text: var(--company-brand-70);
--selaras-color-primary-text-hover: var(--company-brand-80);
--selaras-color-primary-text-pressed: var(--company-brand-90);
--selaras-color-primary-border: var(--company-brand-50);
--selaras-color-primary-focus: var(--company-brand-60);
}
```
Map the neutral foundations as well as component color roles. For example,
if your existing system exposes these surface and divider tokens:
```css
:root {
--selaras-surface-canvas: var(--company-canvas-light);
--selaras-surface-default: var(--company-panel-light);
--selaras-surface-elevated: var(--company-sidebar-light);
--selaras-border-default: var(--company-divider-light);
--selaras-border-muted: var(--company-divider-subtle-light);
--selaras-border-hover: var(--company-divider-hover-light);
}
:root.dark {
--selaras-surface-canvas: var(--company-canvas-dark);
--selaras-surface-default: var(--company-panel-dark);
--selaras-surface-elevated: var(--company-sidebar-dark);
--selaras-border-default: var(--company-divider-dark);
--selaras-border-muted: var(--company-divider-subtle-dark);
--selaras-border-hover: var(--company-divider-hover-dark);
}
```
Define the `--company-*` variables in your own system. Choose divider values
that remain visible on the panels and sidebar where you need a boundary; do
not automatically map a border to the same scale entry as its background.
See [Check borders against their surfaces](https://selarasui.my.id/#check-borders-against-their-surfaces).
Your design system stays the source of truth. The `--selaras-*` variables are
the mapping layer.
### Use your own complete theme
Most applications should import the default theme:
```css
@import "tailwindcss";
@import "@sewadah/selaras";
@import "#selaras/tailwind.css";
```
A mature design system can omit Selaras-owned foundation colors:
```css
@import "tailwindcss";
@import "@sewadah/selaras/structural.css";
@import "#selaras/tailwind.css";
```
Then provide all semantic values that your application uses.
`structural.css` still supplies:
- Selaras source discovery for Tailwind
- variants
- animations
- structural CSS
- semantic read bindings
It does not supply the default Selaras foundation palette.
This mode is for a complete external theme. It is not an automatic unthemed
fallback.
### Radius, shadows, stacking, and Tailwind tokens
Radius uses one base input by default:
```css
:root {
--selaras-radius-base: 0.25rem;
}
```
Selaras derives its default small, medium, and large radius values from this
base. You can override each radius input separately if you need to.
You can also set shadows and stacking values:
```css
:root {
--selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
--selaras-shadow-md: 0 4px 16px -2px rgb(0 0 0 / 0.12);
--selaras-shadow-lg: 0 12px 32px -4px rgb(0 0 0 / 0.16);
--selaras-z-modal-overlay: 40;
--selaras-z-modal: 50;
--selaras-z-dropdown: 60;
--selaras-z-tooltip: 70;
--selaras-z-toast: 80;
}
```
Spacing and normal Tailwind breakpoints are Tailwind tokens, not Selaras theme
tokens:
```css
@theme {
--breakpoint-3xl: 1920px;
--spacing: 0.2rem;
}
```
The adaptive Selaras presentation uses the Tailwind breakpoint name selected by
`selaras.adaptive.breakpoint`. The default name is `md`.
# Global configuration
## Global overrides
Use `app.config.ts` when you want global component class changes or supported
component defaults.
This configuration is not required for normal CSS theme customization.
### Global component classes
```ts
export default defineAppConfig({
selaras: {
ui: {
button: {
slots: {
base: 'font-mono',
},
},
},
},
})
```
Selaras extends the component `tailwind-variants` recipe with these classes.
### Global component defaults
Only components that support theme defaults read this configuration.
For example:
```ts
export default defineAppConfig({
selaras: {
defaults: {
button: {
size: 'lg',
},
},
},
})
```
An explicit component prop has higher priority than a configured default.
### Global runtime tokens
`app.config.selaras.tokens` can also supply runtime token overrides.
Use this only when you need runtime-managed values. Prefer CSS for a static
global design theme.
All app-wide Selaras runtime settings use the `app.config.selaras` namespace.
# Theming
Selaras separates semantic CSS tokens, component styles, and behavior defaults. You can change a few values or supply a complete design system.
| Goal | Guide |
| ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| Change surfaces, typography, radius, or shadows | [CSS tokens](https://selarasui.my.id/overview/theming/css-tokens) |
| Customize semantic colors or register a role | [Color roles](https://selarasui.my.id/overview/theming/color-roles) |
| Follow system appearance or switch light/dark | [Color modes](https://selarasui.my.id/overview/theming/color-modes) |
| Style one component or slot | [Component styling](https://selarasui.my.id/overview/theming/component-styling) |
| Share styles and defaults across the app | [Global configuration](https://selarasui.my.id/overview/theming/global-configuration) |
| Style a subtree independently | [Scoped themes](https://selarasui.my.id/overview/theming/scoped-themes) |
| Supply every theme input yourself | [Complete themes](https://selarasui.my.id/overview/theming/complete-themes) |
Start with the aggregate CSS import from [Installation](https://selarasui.my.id/overview/getting-started/installation). It supplies the default theme. For a complete owned theme, the structural entry retains component behavior and bindings while you provide the theme contract.
The documentation's Pelog, Slendro, and Degung presets demonstrate CSS and component configuration working together. They are documentation showcase themes, not additional named themes automatically installed into your application.
# Scoped themes
### Scoped component configuration
Use `STheme` to scope `ui` and supported default props to a subtree:
```vue-html
Scoped
```
Use a DOM boundary when you also need runtime token overrides:
```vue-html
Scoped color
```
Runtime tokens are useful for dynamic or scoped themes. They do not replace the
normal CSS-first global theme path.
Supported overlay portals carry managed Selaras theme values into the portal
scope. External CSS custom properties still follow normal DOM inheritance.
# Calendar
## Usage
`SCalendar` is an inline date grid. Its value is an `@internationalized/date``DateValue`, as in `SDatePicker`. The selected date and the displayed month are
separate: use `v-model` for selection and `v-model:placeholder` when you need to
control the displayed month.
```vue
```
```vue
```
Use `default-value` and `default-placeholder` for initial uncontrolled state.
`min-value`, `max-value`, `is-date-disabled`, and `is-date-unavailable` control
which dates can be selected. A holiday can be shown without marking its date
unavailable.
### Meeting content
`#day` replaces the content *inside* the date button; it receives the date,
localized day label, and selected/today/disabled/unavailable state. Use it for
compact markers. `#day-details` renders beside that button within the day cell,
so meeting links or buttons do not become nested controls. Selaras associates
that detail area with the date button for assistive technology. The calendar
does not own your events or their data format.
```vue
{{ meeting }}
```
```vue
{{ meeting }}
```
The full month grid needs room on narrow screens. A consumer can scroll its
container, shorten the in-cell preview, or place the complete meeting list
below the calendar. If meetings have their own actions, render actual links or
buttons in `#day-details` and provide an accessible name for each action.
### Schedule composition
The same slots can support a larger schedule: the date sits at the top right
of each day cell, a timed event uses `SBadge`'s status dot, an all-day event
uses its soft badge, and a multi-day event is split into visual segments at
week boundaries. The whole cell selects a date, while each event opens a
popover with details and attendee avatars. This layout is an example-level
`ui` override; the compact calendar keeps centered dates by default.
```vue
```
The fixture uses a fixed September 2026 date as its highlighted sample
"today" so its SSR and static output stay deterministic. In an application,
derive that date from your own time-zone-aware state. The event records,
week-segment calculation, and scoped layout styles are all in the
[complete example source](https://github.com/sewadahapp/selaras/blob/master/docs/components/content/examples/calendar/CalendarSchedule.vue){rel=""nofollow""}.
### Styling
`ui` targets `root`, `header`, `heading`, `grids`, `grid`, `gridHead`,
`headCell`, `cell`, `dayButton`, `dayDetails`, and `footer`. `color` styles
unselected day/navigation buttons; `active-color` styles the selected day.
The `#footer` slot adds content below the grids. Each month is compact by
default; use `class="w-full"` with `:ui="{ grid: 'w-full' }"` for a calendar
that fills its container.
# Table
## Usage
Pass `data` and declare columns with `` children - no column config
objects required for the common case:
```vue
```
```vue
```
Columns are not sortable by default. Add `sortable` to an `` to
enable sorting; an adjacent icon shows its current state (unsorted, ascending,
or descending). Click a sortable header, or focus it and press
:kbd[Enter] / :kbd[Space], to cycle through those states. Shift-click
a second sortable header to add it as a secondary sort without clearing the
first. `filterable` adds a funnel button beside that column's header; it opens
a popover with a live text filter and a Clear action. `selectable` adds a leading checkbox column wired to
`v-model:row-selection`.
Use `select-on-row-click` alongside `selectable` to toggle selection by
clicking anywhere on a row. Clicks on controls inside cells (such as links,
buttons, and checkboxes) keep their normal behavior. `row-hover` adds a subtle
theme-aware background while the pointer is over a body row:
```vue
```
```vue-html
```
Selection and expansion use TanStack's positional row ids by default. For
data that can be sorted, filtered, refreshed, or paginated by a server, provide
`get-row-id` so controlled state follows the record rather than its current
array position:
```vue-html
```
`STable` doesn't render a global search input itself - wire one up yourself
and bind it to `v-model:global-filter`, same compositional approach as the
rest of the library.
### Grouped headers
Wrap columns in `` to produce a multi-row header with the
correct `colspan`/`rowspan` - nest as deeply as needed:
```vue
```
```vue-html
```
### Pagination
Pagination is opt-in: every row renders on one page until you set
`page-size`. Once set, a [Pagination](https://selarasui.my.id/components/navigation/pagination)
control (page numbers, Prev/Next, scaling with `size`) appears automatically
once there's more than one page, and binds to `v-model:page-index` if you
need to read or control the current page yourself. `virtualize` is the
better fit for windowing a large dataset instead - see
[Virtual scroll](https://selarasui.my.id/#virtual-scroll) below.
### Presort
`default-sorting` seeds the initial sort without needing `v-model:sorting` -
useful when you want a table to open already sorted but still let the user
change it freely afterward:
```vue-html
```
Unlike `sorting`, this is read once as a starting value, not on every render -
the same uncontrolled-default pattern as `Accordion`/`Tabs`'s `defaultValue`.
### Custom cell content
Give an `` a default slot to customize how each cell renders. It
receives `{ row, value }`, where `row` is the full row object and `value` is
that column's own field value:
```vue-html
```
### Custom header content
``/`` take the same treatment for their `header`/
`footer` - a named slot (scoped with `{ column }`, TanStack's own column
API) that replaces the plain string prop when given:
```vue
Role
```
```vue-html
Role
```
### Row expansion
`expandable` adds a leading toggle column; give `STable` an `expanded` slot
for the detail content, receiving `{ row }` (the row's original data object):
```vue
Items: {{ (row as Order).items.join(', ') }}
```
```vue-html
Items: {{ row.items.join(', ') }}
```
Expansion state is uncontrolled by default; bind `v-model:expanded` if you
need to read or drive it yourself (e.g. to expand a row programmatically).
### Tree data
Set `tree` to render hierarchical rows from each row's `subRows` array. Parent
rows get an inline expand control in the first visible data column, and nested
rows are indented by depth. For another child property, pass `get-sub-rows`.
Use stable `get-row-id` values when the same table may expand, select, or sort
tree rows. Tree mode uses `expanded` for child visibility, so it cannot be
combined with the detail `#expanded` slot.
```vue
```
```vue-html
```
For a different nested field, return each row's children with
`get-sub-rows="row => row.children"`.
### Row click
`@row-click`/`@row-contextmenu` fire with the row's original data object
and the native event - useful for navigating to a detail page, for
instance. A row only gets a pointer cursor when `@row-click` actually has
a listener attached, so a plain, non-interactive table isn't misleadingly
styled as clickable:
```vue
Selected: {{ selected.name }}
```
```vue-html
router.push(`/users/${row.id}`)">
```
### Per-row styling
`row-class`/`row-style` are called with each row's own data, letting you
style a row conditionally - highlighting an overdue order, say:
```vue
```
```vue-html
```
### Column visibility toggle
`column-toggle` renders a "Columns" button above the table that opens a panel
of checkboxes, one per column, letting the user hide/show columns on the fly:
```vue
```
```vue-html
```
Bind `v-model:column-visibility` if you need to read or control which
columns are shown yourself (e.g. to persist the choice).
### Frozen columns
Give a column `pinned="left"` or `pinned="right"` to keep it fixed at that
edge during horizontal scroll - pair it with `scroll-height` (below) and
enough columns (or a `ui.table` min-width override) that the table actually
needs to scroll:
```vue
Edit
Delete
```
```vue-html
Edit
```
Sticky offsets are measured from each header's real rendered width after
mount and whenever the column list or visibility changes - not on every
possible layout shift (a later web-font load, a window resize), which is a
deliberate scope cut for this pass.
### Virtual scroll
`scroll-height` caps the table at a fixed CSS height with a sticky header and
vertical scroll. Add `virtualize` to only render the rows currently in view,
for datasets too large to put in the DOM all at once:
```vue
```
```vue-html
```
Pass an object instead of `true` to tune `estimateSize`/`overscan`:
```vue-html
```
Omit either field (or pass `virtualize` as plain `true`) and it defaults to
`estimateSize: 40, overscan: 8`.
Virtualizing replaces pagination as the strategy for a large dataset - while
`virtualize` is set, `page-size` has no effect and the pagination controls
don't render.
### Sizes, grid lines & striped rows
`size` (`sm` / `md` / `lg`) controls cell padding and text size. `gridlines`
adds borders around every cell instead of just row dividers, and `striped`
alternates row backgrounds:
```vue
```
Gridline tables keep one rounded outer frame around the scroll viewport, with
lines between columns and rows. The same frame remains around empty and loading
states; no extra table border is added. Use `ui.wrapper` to customize the frame
and `ui.th` / `ui.td` for cell lines.
```vue-html
```
### Loading
`loading` overlays the table with a spinner while keeping the existing rows
visible underneath, rather than swapping to an empty state:
```vue
```
```vue-html
```
### Export
`STable` exposes an `exportCsv(filename?)` method via a template ref - it
downloads every row matching the current filters/sort across all pages (not
just the current page), skipping any synthetic selection/expand column:
```vue
Export CSV
```
```vue-html
Export CSV
```
### Server-side / manual mode
By default, `STable` sorts/filters/paginates `data` itself, locally, every
time. Set `manual-sorting`/`manual-filtering`/`manual-pagination` when
`data` is already sorted/filtered/paginated server-side instead - each one
opts that specific concern out of the local row model (TanStack's term for
the actual list of rows it computes after applying sorting/filtering/
pagination), so `STable` doesn't redundantly (and incorrectly) reprocess a
slice the server already handled. `sorting`/`global-filter`/`page-index` still drive the UI and
still emit their own `update:*` events the normal way; only the local row
model backing them changes.
`manual-pagination` also suppresses the built-in pagination UI entirely -
there's no way to derive a real page count from a server-paginated slice
of `data`, and guessing at one would be misleading. Bring your own
[SPagination](https://selarasui.my.id/components/navigation/pagination) instead, bound to
whatever your server actually returns, the same way this table already
expects you to bring your own search input for `global-filter` (see
[Usage](https://selarasui.my.id/#usage) above) rather than rendering one itself:
```vue-html
pageIndex = page - 1"
/>
```
### Escape hatch: raw column defs
For full TanStack type inference (or features `` doesn't expose),
pass a `columns` prop instead of `` children. Raw column definitions
also start non-sortable; set `enableSorting: true` on columns that should sort.
`STable` infers its row type from `data`, and `TableColumnDef` checks
accessor keys and render contexts against that same type.
`createTableColumnHelper()` is
auto-imported by the Nuxt module and binds TanStack v9's feature generic to the
fixed feature set used by `STable`:
```vue
```
## Known limitation
``'s `field` prop remains a plain `string`, rather than `keyof TData` -
this is a genuine Vue limitation (a parent's generic type parameter doesn't
propagate into child components referenced in its template), not something
unique to `STable` - any declarative-column-as-children table API in Vue has
the identical gap. As a cheap safety net, `STable` warns in dev mode (not
production) if a column's `field` doesn't exist as a key on the first row of
`data` - catching typos without any type-system gymnastics. For actual
compile-time safety, use the `columns` escape hatch above instead.
The table component itself is generic. Its `columns`, `getRowId`, `rowClass`,
`rowStyle`, row events, and `expanded` slot all share the row type inferred
from `data`.
### Accessibility
Sortable headers are keyboard-operable ( :kbd[Enter] / :kbd[Space]
toggle the sort, same as a click) and expose `aria-sort` reflecting the
current state. The row-expansion toggle is a real `