diff --git a/src/uix/eidos/components/skeleton/index.ts b/src/uix/eidos/components/skeleton/index.ts
new file mode 100644
index 000000000..416c7cfa6
--- /dev/null
+++ b/src/uix/eidos/components/skeleton/index.ts
@@ -0,0 +1,12 @@
+// Skeleton — content placeholder primitive.
+//
+// import { Skeleton } from '$uix/eidos/components/skeleton';
+//
+//
+//
+//
+import Skeleton from './skeleton.svelte';
+
+export { Skeleton };
+export default Skeleton;
+export type { SkeletonProps, SkeletonShape, SkeletonSize, SkeletonColor } from './types';
diff --git a/src/uix/eidos/components/skeleton/langs.ts b/src/uix/eidos/components/skeleton/langs.ts
new file mode 100644
index 000000000..059434f5f
--- /dev/null
+++ b/src/uix/eidos/components/skeleton/langs.ts
@@ -0,0 +1,4 @@
+/** Idlangref constants for the Skeleton component. */
+export const SKELETON_LANGS = {
+ LABEL: '#?components.skeleton.label|Loading'
+} as const;
diff --git a/src/uix/eidos/components/skeleton/skeleton.css b/src/uix/eidos/components/skeleton/skeleton.css
new file mode 100644
index 000000000..67b5ead07
--- /dev/null
+++ b/src/uix/eidos/components/skeleton/skeleton.css
@@ -0,0 +1,144 @@
+/*
+ * Skeleton — placeholder block.
+ *
+ * Shapes:
+ * - rect → rounded rectangle, sized by --_skeleton-{w,h}
+ * - circle → 1:1 ratio, fully rounded
+ * - text → stacked thin bars at staggered widths
+ *
+ * Animation: optional shimmer wave via background-position keyframes.
+ */
+[data-skeleton] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-neutral-track) 60%, transparent);
+ --_skeleton-highlight: color-mix(in srgb, var(--color-neutral-track) 100%, white 10%);
+ --_skeleton-size: 1rem;
+ --_skeleton-radius: var(--radius-md);
+ --_skeleton-duration: 1.4s;
+
+ display: block;
+ background: var(--_skeleton-bg);
+ border-radius: var(--_skeleton-radius);
+ inline-size: 100%;
+ block-size: var(--_skeleton-size);
+ overflow: hidden;
+ position: relative;
+}
+
+/* Sizes — intrinsic block-size when the consumer doesn't override. */
+[data-skeleton][data-size='xs'] {
+ --_skeleton-size: 0.5rem;
+}
+[data-skeleton][data-size='sm'] {
+ --_skeleton-size: 0.75rem;
+}
+[data-skeleton][data-size='md'] {
+ --_skeleton-size: 1rem;
+}
+[data-skeleton][data-size='lg'] {
+ --_skeleton-size: 1.5rem;
+}
+[data-skeleton][data-size='xl'] {
+ --_skeleton-size: 2rem;
+}
+
+/* ── Shapes ────────────────────────────────────────────────────────── */
+
+[data-skeleton][data-shape='rect'] {
+ --_skeleton-radius: var(--radius-md);
+}
+
+[data-skeleton][data-shape='circle'] {
+ --_skeleton-radius: 50%;
+ aspect-ratio: 1 / 1;
+ inline-size: var(--_skeleton-size);
+}
+
+[data-skeleton][data-shape='text'] {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--_skeleton-size) * 0.5);
+ background: transparent;
+ overflow: visible;
+ block-size: auto;
+}
+
+[data-skeleton-line] {
+ display: block;
+ block-size: var(--_skeleton-size, 1rem);
+ border-radius: var(--radius-sm);
+ background: var(--_skeleton-bg);
+ overflow: hidden;
+ position: relative;
+}
+
+/* ── Color accents ─────────────────────────────────────────────────── */
+
+[data-skeleton][data-color='primary'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-primary-track) 50%, transparent);
+ --_skeleton-highlight: color-mix(in srgb, var(--color-primary-track) 90%, white 8%);
+}
+[data-skeleton][data-color='secondary'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-secondary-track) 50%, transparent);
+ --_skeleton-highlight: color-mix(in srgb, var(--color-secondary-track) 90%, white 8%);
+}
+[data-skeleton][data-color='affirm'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-affirm-track) 50%, transparent);
+}
+[data-skeleton][data-color='fulfill'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-fulfill-track) 50%, transparent);
+}
+[data-skeleton][data-color='risk'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-risk-track) 50%, transparent);
+}
+[data-skeleton][data-color='threat'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-threat-track) 50%, transparent);
+}
+[data-skeleton][data-color='loss'] {
+ --_skeleton-bg: color-mix(in srgb, var(--color-loss-track) 50%, transparent);
+}
+
+/* ── Animation (shimmer wave) ──────────────────────────────────────── */
+
+[data-skeleton][data-animated] {
+ background-image: linear-gradient(
+ 90deg,
+ var(--_skeleton-bg) 0%,
+ var(--_skeleton-highlight) 50%,
+ var(--_skeleton-bg) 100%
+ );
+ background-size: 200% 100%;
+ animation: skeleton-shimmer var(--_skeleton-duration) ease-in-out infinite;
+}
+
+[data-skeleton][data-shape='text'][data-animated] {
+ background-image: none;
+ animation: none;
+}
+
+[data-skeleton][data-shape='text'][data-animated] [data-skeleton-line] {
+ background-image: linear-gradient(
+ 90deg,
+ var(--_skeleton-bg) 0%,
+ var(--_skeleton-highlight) 50%,
+ var(--_skeleton-bg) 100%
+ );
+ background-size: 200% 100%;
+ animation: skeleton-shimmer var(--_skeleton-duration) ease-in-out infinite;
+}
+
+@keyframes skeleton-shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-skeleton][data-animated],
+ [data-skeleton][data-shape='text'][data-animated] [data-skeleton-line] {
+ animation: none;
+ background-image: none;
+ }
+}
diff --git a/src/uix/eidos/components/skeleton/skeleton.svelte b/src/uix/eidos/components/skeleton/skeleton.svelte
new file mode 100644
index 000000000..2d15dd5cc
--- /dev/null
+++ b/src/uix/eidos/components/skeleton/skeleton.svelte
@@ -0,0 +1,81 @@
+
+
+{#if resolvedShape === 'text' && lineWidths}
+
+ {#each lineWidths as w, i (i)}
+
+ {/each}
+
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/uix/eidos/components/skeleton/types.ts b/src/uix/eidos/components/skeleton/types.ts
new file mode 100644
index 000000000..dd17735af
--- /dev/null
+++ b/src/uix/eidos/components/skeleton/types.ts
@@ -0,0 +1,35 @@
+import type { Snippet } from 'svelte';
+import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
+import type { BoxProps } from '../box/types';
+
+export type SkeletonShape = 'rect' | 'circle' | 'text';
+export type SkeletonSize = Extract;
+export type SkeletonColor = ColorRole;
+
+/**
+ * Props for ``.
+ *
+ * Composes through `` so every layout prop (width, height,
+ * gridColumn, …) flows through. The `shape` prop only adjusts the
+ * recipe (border-radius, intrinsic block-size). For text skeletons,
+ * pass `lines={n}` to stack n text bars stagger-widthed.
+ */
+export type SkeletonProps = Omit & {
+ /** Visual shape. @default 'rect' */
+ shape?: ResponsiveProp;
+ /** Reservation size. @default 'md' (only meaningful when no width/height set) */
+ size?: ResponsiveProp;
+ /** Accent palette (placeholder background lightness). @default 'neutral' */
+ color?: SkeletonColor;
+ /**
+ * Number of stacked bars when `shape='text'`. Each bar's width
+ * decreases slightly so the block looks like a paragraph.
+ * @default 1
+ */
+ lines?: number;
+ /** Animate the shimmer pulse. @default true */
+ animated?: boolean;
+ /** Override the SR text. Defaults to a translated `'Loading'`. */
+ 'aria-label'?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/spinner/index.ts b/src/uix/eidos/components/spinner/index.ts
new file mode 100644
index 000000000..cffcdb7e4
--- /dev/null
+++ b/src/uix/eidos/components/spinner/index.ts
@@ -0,0 +1,12 @@
+// Spinner — indeterminate progress indicator.
+//
+// import { Spinner } from '$uix/eidos/components/spinner';
+//
+//
+//
+// Loading…
+import Spinner from './spinner.svelte';
+
+export { Spinner };
+export default Spinner;
+export type { SpinnerProps, SpinnerSize, SpinnerColor, SpinnerVariant } from './types';
diff --git a/src/uix/eidos/components/spinner/langs.ts b/src/uix/eidos/components/spinner/langs.ts
new file mode 100644
index 000000000..2d639c0ad
--- /dev/null
+++ b/src/uix/eidos/components/spinner/langs.ts
@@ -0,0 +1,4 @@
+/** Idlangref constants for the Spinner component. */
+export const SPINNER_LANGS = {
+ LABEL: '#?components.spinner.label|Loading'
+} as const;
diff --git a/src/uix/eidos/components/spinner/spinner.css b/src/uix/eidos/components/spinner/spinner.css
new file mode 100644
index 000000000..82da6e2c1
--- /dev/null
+++ b/src/uix/eidos/components/spinner/spinner.css
@@ -0,0 +1,194 @@
+/*
+ * Spinner — indeterminate progress.
+ *
+ * Three variants share the same outer chrome:
+ * - ring → rotating arc made by a conic-gradient mask
+ * - dots → 3 dots, staggered scale animation
+ * - bars → 4 bars, staggered height animation
+ *
+ * Reduced motion: animations stop but the element stays visible so the
+ * "loading" affordance is preserved.
+ */
+[data-spinner] {
+ --_spinner-size: 1.25rem;
+ --_spinner-thickness: 2px;
+ --_spinner-duration: 0.9s;
+ --_spinner-color: var(--color-primary-text);
+ --_spinner-track: color-mix(in srgb, var(--color-primary-track) 60%, transparent);
+ --_spinner-gap: var(--space-2);
+
+ display: inline-flex;
+ align-items: center;
+ gap: var(--_spinner-gap);
+ color: var(--_spinner-color);
+}
+
+[data-spinner][data-size='xs'] {
+ --_spinner-size: 0.75rem;
+ --_spinner-thickness: 1.5px;
+}
+[data-spinner][data-size='sm'] {
+ --_spinner-size: 1rem;
+ --_spinner-thickness: 2px;
+}
+[data-spinner][data-size='md'] {
+ --_spinner-size: 1.25rem;
+ --_spinner-thickness: 2px;
+}
+[data-spinner][data-size='lg'] {
+ --_spinner-size: 1.75rem;
+ --_spinner-thickness: 3px;
+}
+[data-spinner][data-size='xl'] {
+ --_spinner-size: 2.5rem;
+ --_spinner-thickness: 4px;
+}
+
+/* Color tokens — accent + track. */
+[data-spinner][data-color='secondary'] {
+ --_spinner-color: var(--color-secondary-text);
+ --_spinner-track: color-mix(in srgb, var(--color-secondary-track) 60%, transparent);
+}
+[data-spinner][data-color='neutral'] {
+ --_spinner-color: var(--color-neutral-text);
+ --_spinner-track: color-mix(in srgb, var(--color-neutral-track) 60%, transparent);
+}
+[data-spinner][data-color='affirm'] {
+ --_spinner-color: var(--color-affirm-text);
+ --_spinner-track: color-mix(in srgb, var(--color-affirm-track) 60%, transparent);
+}
+[data-spinner][data-color='fulfill'] {
+ --_spinner-color: var(--color-fulfill-text);
+ --_spinner-track: color-mix(in srgb, var(--color-fulfill-track) 60%, transparent);
+}
+[data-spinner][data-color='risk'] {
+ --_spinner-color: var(--color-risk-text);
+ --_spinner-track: color-mix(in srgb, var(--color-risk-track) 60%, transparent);
+}
+[data-spinner][data-color='threat'] {
+ --_spinner-color: var(--color-threat-text);
+ --_spinner-track: color-mix(in srgb, var(--color-threat-track) 60%, transparent);
+}
+[data-spinner][data-color='loss'] {
+ --_spinner-color: var(--color-loss-text);
+ --_spinner-track: color-mix(in srgb, var(--color-loss-track) 60%, transparent);
+}
+
+/* ── Ring variant ──────────────────────────────────────────────────── */
+
+[data-spinner][data-variant='ring'] [data-spinner-track] {
+ display: block;
+ inline-size: var(--_spinner-size);
+ block-size: var(--_spinner-size);
+ border-radius: 50%;
+ border: var(--_spinner-thickness) solid var(--_spinner-track);
+ border-top-color: var(--_spinner-color);
+ animation: spinner-rotate var(--_spinner-duration) linear infinite;
+}
+
+/* ── Dots variant ──────────────────────────────────────────────────── */
+
+[data-spinner][data-variant='dots'] [data-spinner-track] {
+ display: inline-flex;
+ align-items: center;
+ gap: calc(var(--_spinner-size) * 0.2);
+ block-size: var(--_spinner-size);
+}
+
+[data-spinner][data-variant='dots'] [data-spinner-dot] {
+ display: block;
+ inline-size: calc(var(--_spinner-size) * 0.3);
+ block-size: calc(var(--_spinner-size) * 0.3);
+ border-radius: 50%;
+ background: var(--_spinner-color);
+ animation: spinner-bounce 1.2s ease-in-out infinite both;
+}
+
+[data-spinner][data-variant='dots'] [data-spinner-dot]:nth-child(1) {
+ animation-delay: -0.32s;
+}
+[data-spinner][data-variant='dots'] [data-spinner-dot]:nth-child(2) {
+ animation-delay: -0.16s;
+}
+
+/* ── Bars variant ──────────────────────────────────────────────────── */
+
+[data-spinner][data-variant='bars'] [data-spinner-track] {
+ display: inline-flex;
+ align-items: center;
+ gap: calc(var(--_spinner-size) * 0.12);
+ block-size: var(--_spinner-size);
+}
+
+[data-spinner][data-variant='bars'] [data-spinner-bar] {
+ display: block;
+ inline-size: calc(var(--_spinner-size) * 0.18);
+ block-size: var(--_spinner-size);
+ border-radius: var(--radius-sm);
+ background: var(--_spinner-color);
+ animation: spinner-stretch 1s ease-in-out infinite;
+}
+
+[data-spinner][data-variant='bars'] [data-spinner-bar]:nth-child(1) {
+ animation-delay: -1s;
+}
+[data-spinner][data-variant='bars'] [data-spinner-bar]:nth-child(2) {
+ animation-delay: -0.85s;
+}
+[data-spinner][data-variant='bars'] [data-spinner-bar]:nth-child(3) {
+ animation-delay: -0.7s;
+}
+[data-spinner][data-variant='bars'] [data-spinner-bar]:nth-child(4) {
+ animation-delay: -0.55s;
+}
+
+/* ── Label ─────────────────────────────────────────────────────────── */
+
+[data-spinner-label] {
+ font-size: var(--font-size-sm);
+ color: var(--color-content-muted);
+}
+
+/* ── Keyframes ─────────────────────────────────────────────────────── */
+
+@keyframes spinner-rotate {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes spinner-bounce {
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0.5);
+ opacity: 0.5;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes spinner-stretch {
+ 0%,
+ 40%,
+ 100% {
+ transform: scaleY(0.4);
+ }
+ 20% {
+ transform: scaleY(1);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-spinner] [data-spinner-track],
+ [data-spinner] [data-spinner-dot],
+ [data-spinner] [data-spinner-bar] {
+ animation: none;
+ }
+ [data-spinner][data-variant='ring'] [data-spinner-track] {
+ border-top-color: var(--_spinner-track);
+ opacity: 0.6;
+ }
+}
diff --git a/src/uix/eidos/components/spinner/spinner.svelte b/src/uix/eidos/components/spinner/spinner.svelte
new file mode 100644
index 000000000..f6ba54d06
--- /dev/null
+++ b/src/uix/eidos/components/spinner/spinner.svelte
@@ -0,0 +1,64 @@
+
+
+
+ {#if variant === 'dots'}
+
+
+
+
+
+ {:else if variant === 'bars'}
+
+
+
+
+
+
+ {:else}
+
+ {/if}
+ {#if children}
+ {@render children()}
+ {/if}
+
diff --git a/src/uix/eidos/components/spinner/types.ts b/src/uix/eidos/components/spinner/types.ts
new file mode 100644
index 000000000..5b55af4f2
--- /dev/null
+++ b/src/uix/eidos/components/spinner/types.ts
@@ -0,0 +1,28 @@
+import type { Snippet } from 'svelte';
+import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
+import type { BoxProps } from '../box/types';
+
+export type SpinnerSize = Extract;
+export type SpinnerColor = ColorRole;
+export type SpinnerVariant = 'ring' | 'dots' | 'bars';
+
+/**
+ * Props for ``.
+ *
+ * Composes through `` for layout primitives (gap, gridColumn, …).
+ * The `variant` prop toggles between three canonical visual styles —
+ * a rotating ring (default, lightest), bouncing dots (medium weight),
+ * scrolling bars (most attention).
+ */
+export type SpinnerProps = Omit & {
+ /** Visual style. @default 'ring' */
+ variant?: SpinnerVariant;
+ /** Spinner size. @default 'md' */
+ size?: ResponsiveProp;
+ /** Accent palette. @default 'primary' */
+ color?: SpinnerColor;
+ /** Override the SR text. Defaults to a translated `'Loading'`. */
+ 'aria-label'?: string;
+ /** Optional visible label rendered next to the spinner. */
+ children?: Snippet;
+};
diff --git a/src/uix/langs/components/index.ts b/src/uix/langs/components/index.ts
index 8e9676bb7..97ef74c2f 100644
--- a/src/uix/langs/components/index.ts
+++ b/src/uix/langs/components/index.ts
@@ -45,7 +45,9 @@ import { ratingGroupLangs } from './rating-group';
import { scrollAreaLangs } from './scroll-area';
import { searchFieldLangs } from './search-field';
import { selectLangs } from './select';
+import { skeletonLangs } from './skeleton';
import { sliderLangs } from './slider';
+import { spinnerLangs } from './spinner';
import { splitterLangs } from './splitter';
import { stepperLangs } from './stepper';
import { switchLangs } from './switch';
@@ -119,7 +121,9 @@ export const componentLangs = {
'scroll-area': scrollAreaLangs,
'search-field': searchFieldLangs,
select: selectLangs,
+ skeleton: skeletonLangs,
slider: sliderLangs,
+ spinner: spinnerLangs,
splitter: splitterLangs,
stepper: stepperLangs,
switch: switchLangs,
diff --git a/src/uix/langs/components/skeleton.ts b/src/uix/langs/components/skeleton.ts
new file mode 100644
index 000000000..1287d1346
--- /dev/null
+++ b/src/uix/langs/components/skeleton.ts
@@ -0,0 +1,12 @@
+import type { LangNode } from '$libs/langs';
+
+/**
+ * Default strings for the skeleton component. Merged under
+ * `components.skeleton.*` by `ActiveUix` (via `componentLangs` barrel).
+ */
+export const skeletonLangs = {
+ label: {
+ es: 'Cargando',
+ en: 'Loading'
+ }
+} satisfies LangNode;
diff --git a/src/uix/langs/components/spinner.ts b/src/uix/langs/components/spinner.ts
new file mode 100644
index 000000000..bb76b5ef2
--- /dev/null
+++ b/src/uix/langs/components/spinner.ts
@@ -0,0 +1,12 @@
+import type { LangNode } from '$libs/langs';
+
+/**
+ * Default strings for the spinner component. Merged under
+ * `components.spinner.*` by `ActiveUix` (via `componentLangs` barrel).
+ */
+export const spinnerLangs = {
+ label: {
+ es: 'Cargando',
+ en: 'Loading'
+ }
+} satisfies LangNode;
diff --git a/src/uix/morfo/components/skeleton.ts b/src/uix/morfo/components/skeleton.ts
new file mode 100644
index 000000000..f7e4ba7a8
--- /dev/null
+++ b/src/uix/morfo/components/skeleton.ts
@@ -0,0 +1,47 @@
+import type { Morfo } from '../types';
+import { v } from '../types';
+
+/**
+ * Skeleton — content placeholder shown while data loads.
+ *
+ * Eidos-native primitive: pure visual / a11y surface, no behavior, no
+ * state, no events. Renders a single `` carrying an
+ * `aria-label` so SR users get "loading" announced. Shape (rect / circle /
+ * text) toggles CSS recipe geometry via `data-shape`.
+ *
+ * Justification for 0-event surface: Skeleton has no commit/emerge/shift
+ * semantic — it disappears when the consumer swaps it for real content.
+ * Adding events would manufacture meaning the primitive doesn't carry.
+ * Same precedent as AspectRatio / Box / Flex / Grid.
+ */
+export const skeletonMorfo = {
+ name: 'Skeleton',
+ kebab: 'skeleton',
+ scope: ['eidos'],
+ texts: {
+ label: '#?components.skeleton.label|Loading'
+ },
+ parts: [
+ {
+ name: 'Provider',
+ kebab: 'provider',
+ archetype: 'provider',
+ kind: 'public',
+ defaultElement: 'div',
+ role: 'status',
+ optional: false,
+ data: [
+ { attr: 'data-shape', value: v.propRef('shape'), severity: 'optional' },
+ { attr: 'data-animated', value: v.propRef('animated'), severity: 'optional' }
+ ],
+ aria: [
+ {
+ attr: 'aria-label',
+ value: v.translationRef('#?components.skeleton.label|Loading'),
+ severity: 'recommended'
+ },
+ { attr: 'aria-busy', value: v.literal('true') }
+ ]
+ }
+ ]
+} as const satisfies Morfo;
diff --git a/src/uix/morfo/components/spinner.ts b/src/uix/morfo/components/spinner.ts
new file mode 100644
index 000000000..c616399cd
--- /dev/null
+++ b/src/uix/morfo/components/spinner.ts
@@ -0,0 +1,54 @@
+import type { Morfo } from '../types';
+import { v } from '../types';
+
+/**
+ * Spinner — indeterminate progress indicator.
+ *
+ * Eidos-native primitive. Single `
` with a CSS-only
+ * rotating glyph. Used wherever a determinate `