Skip to Content
DocumentationBase Components

Base Components

Kevlar has 9 base components. Every one of the 108 component files inherits from exactly one base. The base defines the scaffold — states, modalities, actions, input methods, network handling, timing, animation. The tech lead fills in the project defaults. The component file overrides what is specific. The instance fills what is left.

All 9 bases follow the same pattern: they ship full of MUST_BE_DEFINED sentinels. The tech lead replaces every one.


BaseInteractive

Used by: Button, ActionIcon, CloseButton, CopyButton, FileButton, UnstyledButton, Chip, Switch, Checkbox, Radio, SegmentedControl, and more.

The most complex base. Covers any element a user clicks, taps, or activates.

States (8)

Each state has 4 modalities: visual, audio, haptic, screenreader.

StateWhat it represents
idleDefault resting state
hoverMouse/pointer hovering over the element
focusedElement has keyboard or programmatic focus
pressedElement is being pressed/clicked/tapped
loadingAsync action is in progress
successAsync action succeeded
errorAsync action failed
disabledElement is not interactive

The loading, success, and error states also have an announcement slot (STRING_MUST_BE_DEFINED) for screen reader announcements. These always survive to the instance because the base cannot know what “Loading…” means in context.

States (as shipped)

export const baseInteractiveStates = { idle: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, haptic: FUNCTION_MUST_BE_DEFINED, screenreader: OBJECT_MUST_BE_DEFINED, // { role: 'button' } at minimum }, hover: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, haptic: FUNCTION_MUST_BE_DEFINED, screenreader: OBJECT_MUST_BE_DEFINED, }, focused: { visual: OBJECT_MUST_BE_DEFINED, // must include a focus ring audio: FUNCTION_MUST_BE_DEFINED, haptic: FUNCTION_MUST_BE_DEFINED, screenreader: OBJECT_MUST_BE_DEFINED, }, pressed: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, // click sound? haptic: FUNCTION_MUST_BE_DEFINED, // tap haptic? screenreader: OBJECT_MUST_BE_DEFINED, }, loading: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, haptic: FUNCTION_MUST_BE_DEFINED, screenreader: OBJECT_MUST_BE_DEFINED, // must include liveRegion + busy announcement: STRING_MUST_BE_DEFINED, // per instance }, success: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, // success chime? haptic: FUNCTION_MUST_BE_DEFINED, // success haptic? screenreader: OBJECT_MUST_BE_DEFINED, announcement: STRING_MUST_BE_DEFINED, // per instance }, error: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, // error sound? haptic: FUNCTION_MUST_BE_DEFINED, // error haptic? screenreader: OBJECT_MUST_BE_DEFINED, // must include liveRegion assertive announcement: STRING_MUST_BE_DEFINED, // per instance }, disabled: { visual: OBJECT_MUST_BE_DEFINED, audio: FUNCTION_MUST_BE_DEFINED, haptic: FUNCTION_MUST_BE_DEFINED, screenreader: OBJECT_MUST_BE_DEFINED, // must include disabled state }, };

Actions

export const baseInteractiveActions = { onRageClick: FUNCTION_MUST_BE_DEFINED, onDoubleClick: FUNCTION_MUST_BE_DEFINED, onClickDuringLoading: FUNCTION_MUST_BE_DEFINED, onFocusEscape: FUNCTION_MUST_BE_DEFINED, };
ActionQuestion the tech lead must answer
onRageClickWhat happens when the user clicks rapidly and repeatedly?
onDoubleClickWhat happens on double click?
onClickDuringLoadingWhat happens if they click while an async action is in progress?
onFocusEscapeWhat happens if they tab away during an async operation?

Input Methods

Four input method blocks, all MUST_BE_DEFINED:

export const baseInteractiveInput = { touch: { onTap: FUNCTION_MUST_BE_DEFINED, onDoubleTap: FUNCTION_MUST_BE_DEFINED, onLongPress: FUNCTION_MUST_BE_DEFINED, onSwipe: FUNCTION_MUST_BE_DEFINED, onPinch: FUNCTION_MUST_BE_DEFINED, touchTargetSize: FUNCTION_MUST_BE_DEFINED, instantFeedback: BOOLEAN_MUST_BE_DEFINED, }, mouse: { onLeftClick: FUNCTION_MUST_BE_DEFINED, onRightClick: FUNCTION_MUST_BE_DEFINED, onMiddleClick: FUNCTION_MUST_BE_DEFINED, onDoubleClick: FUNCTION_MUST_BE_DEFINED, onHoverEnter: FUNCTION_MUST_BE_DEFINED, onHoverLeave: FUNCTION_MUST_BE_DEFINED, onScroll: FUNCTION_MUST_BE_DEFINED, onDragAndDrop: FUNCTION_MUST_BE_DEFINED, }, keyboard: { bindings: { Enter: FUNCTION_MUST_BE_DEFINED, Space: FUNCTION_MUST_BE_DEFINED, Escape: FUNCTION_MUST_BE_DEFINED, }, Tab: OBJECT_MUST_BE_DEFINED, // { trap: boolean } focusRing: FUNCTION_MUST_BE_DEFINED, }, remote_dpad: { onSelect: FUNCTION_MUST_BE_DEFINED, onBack: FUNCTION_MUST_BE_DEFINED, onDirectional: FUNCTION_MUST_BE_DEFINED, focusRing: OBJECT_MUST_BE_DEFINED, // use config.tv.focusRing }, };

Network

export const baseInteractiveNetwork = { onFast: FUNCTION_MUST_BE_DEFINED, onSlow: FUNCTION_MUST_BE_DEFINED, onOffline: FUNCTION_MUST_BE_DEFINED, };

Timing

export const baseInteractiveTiming = { timeoutMs: NUMBER_MUST_BE_DEFINED, onTimeout: FUNCTION_MUST_BE_DEFINED, debounceMs: NUMBER_MUST_BE_DEFINED, triggers: [], // empty, filled per instance minLoadTime: null, // null, filled per instance };

Animation

export const baseInteractiveAnimation = { enter: ANIMATION_MUST_BE_DEFINED, exit: ANIMATION_MUST_BE_DEFINED, transitions: { idle_to_hover: ANIMATION_MUST_BE_DEFINED, hover_to_idle: ANIMATION_MUST_BE_DEFINED, idle_to_pressed: ANIMATION_MUST_BE_DEFINED, pressed_to_loading: ANIMATION_MUST_BE_DEFINED, loading_to_success: ANIMATION_MUST_BE_DEFINED, loading_to_error: ANIMATION_MUST_BE_DEFINED, }, microFeedback: ANIMATION_MUST_BE_DEFINED, loadingAnimation: ANIMATION_MUST_BE_DEFINED, };

After the tech lead fills it in

Here is what the states look like after the tech lead makes every decision:

export const baseInteractiveStates = { idle: { visual: { cursor: 'pointer', opacity: 1 }, audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: { role: 'button' }, }, hover: { visual: (ctx) => ({ cursor: 'pointer', opacity: 1, shadow: isTV() ? config.shadows.high : config.shadows.low, translateY: isTV() ? 0 : -1, scale: isTV() ? config.tv.focusScale : 1, }), audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: { role: 'button' }, }, focused: { visual: (ctx) => ({ cursor: 'pointer', opacity: 1, outline: { width: isTV() ? config.tv.focusRing.width : prefersHighContrast() ? 4 : config.focusRing.width, color: config.focusRing.color, offset: isTV() ? config.tv.focusRing.offset : config.focusRing.offset, style: config.focusRing.style, }, ...(prefersHighContrast() && { borderWidth: 3, borderStyle: 'solid', borderColor: config.colors.focus, }), }), audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: { role: 'button' }, }, pressed: { visual: { cursor: 'pointer', scale: 0.97, opacity: 0.95 }, audio: (ctx) => { if (!isSilentMode()) playSound(config.sounds.click); }, haptic: (ctx) => { fireHaptic(config.haptics.tap); }, screenreader: { role: 'button' }, }, loading: { visual: { cursor: 'wait', opacity: 0.9 }, audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: { role: 'button', liveRegion: 'polite', state: { busy: true } }, announcement: STRING_MUST_BE_DEFINED, // stays — per instance }, success: { visual: { cursor: 'default', opacity: 1 }, audio: (ctx) => { if (!isSilentMode()) playSound(config.sounds.success); }, haptic: (ctx) => { fireHaptic(config.haptics.success); }, screenreader: { role: 'button', liveRegion: 'polite' }, announcement: STRING_MUST_BE_DEFINED, // stays — per instance }, error: { visual: { cursor: 'default', opacity: 1 }, audio: (ctx) => { if (!isSilentMode()) playSound(config.sounds.error); }, haptic: (ctx) => { fireHaptic(config.haptics.error); }, screenreader: { role: 'button', liveRegion: 'assertive' }, announcement: STRING_MUST_BE_DEFINED, // stays — per instance }, disabled: { visual: { cursor: 'not-allowed', opacity: 0.5 }, audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: { role: 'button', state: { disabled: true } }, }, };

BaseInput

Used by: TextInput, NumberInput, PasswordInput, Textarea, JsonInput, ColorInput, PinInput, FileInput, NativeSelect, Slider, RangeSlider, Rating, Chip (when used as input), and more.

States (8)

StateWhat it represents
idleEmpty or pre-filled, not focused
hoverMouse hovering over the input
focusedInput has focus, ready for typing
typingUser is actively entering text
validatingInput value is being validated (async)
validValidation passed
invalidValidation failed
disabledInput is not interactive

Each state has the same 4 modalities as BaseInteractive (visual, audio, haptic, screenreader).

Extra Slots

// What happens when the user presses Enter? // TextInput: submit. Textarea: newline. NumberInput: nothing. // The base leaves this as FUNCTION_MUST_BE_DEFINED. keyboard.bindings.Enter: FUNCTION_MUST_BE_DEFINED, // Debounce for validation/search debounceMs: NUMBER_MUST_BE_DEFINED, // Validation callback onValidate: FUNCTION_MUST_BE_DEFINED,

The Enter key decision is critical. The base cannot know if Enter should submit the form, insert a newline, or do nothing. Every input component must decide.

Mobile input font size

All BaseInput components enforce config.typography.mobileInputFontSize (16px) on mobile to prevent iOS Safari from auto-zooming on focus.


BaseStatic

Used by: Text, Title, Badge, Code, Highlight, Mark, Kbd, Blockquote, Table (cells), List, and more.

States (2)

StateWhat it represents
idleDefault display state
focusedWhen the element receives focus (via tabbing or programmatic focus)

Minimal scaffolding. Most slots are () => {} because static elements do not play sounds or fire haptics. But two things are still MUST_BE_DEFINED:

  • Focus ring — even static elements can receive focus. The focus ring must be defined.
  • Screenreader role — every element needs a role. Is it a status? A heading? A note? The base cannot know.
export const baseStaticStates = { idle: { visual: OBJECT_MUST_BE_DEFINED, audio: (ctx) => {}, // static elements don't make sound haptic: (ctx) => {}, // static elements don't vibrate screenreader: OBJECT_MUST_BE_DEFINED, // role must be defined }, focused: { visual: OBJECT_MUST_BE_DEFINED, // focus ring must be defined audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: OBJECT_MUST_BE_DEFINED, }, };

BaseOverlay

Used by: Modal, Drawer, Dialog, Popover, HoverCard, Tooltip, Menu, Select (dropdown), Combobox, ColorPicker (popup), and more.

States (5)

StateWhat it represents
idleOverlay is not visible, not in DOM or hidden
openingTransition from closed to open (animation in progress)
openOverlay is visible and interactive
closingTransition from open to closed (animation in progress)
closedOverlay has finished closing

Extra Slots

// What happens when the user clicks outside the overlay? onClickOutside: FUNCTION_MUST_BE_DEFINED, // What happens when the user scrolls the page behind the overlay? onScrollBehind: FUNCTION_MUST_BE_DEFINED, // What happens when the user presses Escape? onEscape: FUNCTION_MUST_BE_DEFINED, // Focus management focus: { onOpen: FUNCTION_MUST_BE_DEFINED, // where does focus go when overlay opens? onClose: FUNCTION_MUST_BE_DEFINED, // where does focus return when overlay closes? }, // Tab trapping keyboard: { Tab: { trap: BOOLEAN_MUST_BE_DEFINED, // should Tab be trapped inside the overlay? }, },

Every overlay must answer: what happens on click outside? What happens on Escape? Where does focus go? Is Tab trapped? These are not optional. Modals typically trap focus and close on Escape. Tooltips do not trap focus and close on any interaction. The tech lead decides the defaults. The component overrides what is specific.


BaseContainer

Used by: Paper, Card, Group, Stack, Grid, SimpleGrid, Flex, Center, Container, Space, Divider, Fieldset, and more.

States (1)

StateWhat it represents
idleDefault layout state

The simplest base. Containers are layout primitives — they arrange children. They do not have hover, pressed, or loading states. The base exists so containers pass Kevlar validation and have consistent structure.

export const baseContainerStates = { idle: { visual: OBJECT_MUST_BE_DEFINED, audio: (ctx) => {}, haptic: (ctx) => {}, screenreader: OBJECT_MUST_BE_DEFINED, }, };

Even a container needs a screenreader role. Is it a group? A region? A list? The base makes you decide.


BaseFeedback

Used by: Notification, Alert, Toast, Progress, Loader, Skeleton, and more.

States (5)

StateWhat it represents
idleFeedback element is not yet visible
enteringTransition animation into view
visibleFeedback is shown and readable
dismissingTransition animation out of view
dismissedFeedback has been removed

Extra Slots

// Auto-dismiss timing autoDismissMs: NUMBER_MUST_BE_DEFINED, // use config.timing.autoDismissMs or custom // What happens when auto-dismiss timer fires? onAutoDismiss: FUNCTION_MUST_BE_DEFINED, // Swipe to dismiss (mobile) onSwipeToDismiss: FUNCTION_MUST_BE_DEFINED, // Click to dismiss onClickToDismiss: FUNCTION_MUST_BE_DEFINED,

The tech lead must decide: do notifications auto-dismiss? After how long? Can users swipe them away on mobile? Can they click to dismiss? Each answer is a conscious decision.


BaseNavigation

Used by: Tabs, Breadcrumbs, NavLink, Pagination, Stepper, Anchor, and more.

States (5)

StateWhat it represents
idleNavigation item at rest
hoverMouse hovering
focusedKeyboard focus
activeCurrently selected/active item
disabledItem is not interactive

Roving Tabindex

Navigation components use roving tabindex for keyboard interaction. The base scaffolds the keyboard bindings:

keyboard: { bindings: { ArrowUp: FUNCTION_MUST_BE_DEFINED, // move focus to previous item ArrowDown: FUNCTION_MUST_BE_DEFINED, // move focus to next item ArrowLeft: FUNCTION_MUST_BE_DEFINED, // move focus to previous item (horizontal) ArrowRight: FUNCTION_MUST_BE_DEFINED, // move focus to next item (horizontal) Home: FUNCTION_MUST_BE_DEFINED, // move focus to first item End: FUNCTION_MUST_BE_DEFINED, // move focus to last item Enter: FUNCTION_MUST_BE_DEFINED, // activate focused item Space: FUNCTION_MUST_BE_DEFINED, // activate focused item }, },

The tech lead fills these with roving tabindex logic. Arrow keys move focus between items. Home/End jump to first/last. Enter/Space activate.


BaseDisclosure

Used by: Accordion, Spoiler, Collapse, Details, and more.

States (5)

StateWhat it represents
idleDisclosure trigger at rest
hoverMouse hovering over trigger
focusedTrigger has keyboard focus
openContent is expanded/revealed
closedContent is collapsed/hidden

Extra Slots

// What happens when the user toggles the disclosure? onToggle: FUNCTION_MUST_BE_DEFINED, // Expand/collapse animation animation: { expand: ANIMATION_MUST_BE_DEFINED, collapse: ANIMATION_MUST_BE_DEFINED, },

The tech lead decides: what animation plays when an accordion opens? When it closes? Does it slide, fade, or just appear? prefersReducedMotion() must be checked inline.


BaseMedia

Used by: Image, Avatar, BackgroundImage, and more.

States (4)

StateWhat it represents
idleBefore loading begins
loadingMedia is being fetched
loadedMedia has loaded successfully
errorMedia failed to load

Extra Slots

// Network strategy network: { onFast: FUNCTION_MUST_BE_DEFINED, // load full resolution? preload? onSlow: FUNCTION_MUST_BE_DEFINED, // load lower resolution? show LQIP? onOffline: FUNCTION_MUST_BE_DEFINED, // show cached? show placeholder? show nothing? }, // Fallback chain fallback: { onError: FUNCTION_MUST_BE_DEFINED, // what to show when the image fails to load },

The tech lead must decide the project’s image loading strategy:

  • LQIP (Low Quality Image Placeholder) — show a tiny blurred version while the full image loads?
  • Network adaptation — serve smaller images on slow connections?
  • Offline behavior — show cached images? A placeholder icon? Nothing?
  • Error fallback — a broken image icon? A colored rectangle? A default avatar?
// Example: after tech lead fills BaseMedia network: { onFast: (ctx) => { ctx.loadFullResolution(); }, onSlow: (ctx) => { ctx.loadLowResolution(); ctx.showLQIP(); }, onOffline: (ctx) => { if (ctx.hasCached()) { ctx.showCached(); } else { ctx.showPlaceholder(); } }, }, fallback: { onError: (ctx) => { ctx.showFallbackIcon(); }, },

Summary

BaseStatesKey extras
BaseInteractive8 (idle, hover, focused, pressed, loading, success, error, disabled)Actions (rage click, double click, click during loading, focus escape), 4 input methods, network, timing, animation
BaseInput8 (idle, hover, focused, typing, validating, valid, invalid, disabled)Enter key decision, debounceMs, onValidate
BaseStatic2 (idle, focused)Focus ring, screenreader role
BaseOverlay5 (idle, opening, open, closing, closed)onClickOutside, onScrollBehind, onEscape, focus management, Tab trap
BaseContainer1 (idle)Screenreader role
BaseFeedback5 (idle, entering, visible, dismissing, dismissed)autoDismissMs, onSwipeToDismiss, onClickToDismiss
BaseNavigation5 (idle, hover, focused, active, disabled)Roving tabindex (ArrowUp/Down, Home/End)
BaseDisclosure5 (idle, hover, focused, open, closed)onToggle, expand/collapse animation
BaseMedia4 (idle, loading, loaded, error)Network strategy, LQIP, fallback chain
Last updated on