ui
FreeIsometricField
A text input rendered as an isometric slab — the natural companion to IsometricButton, sharing its camera. The depth edge lights up in your accent color on focus.
// preview
// settings
Thickness (px)
Corner radius (px)
Font size (px)
▸advanced
Camera tilt X (deg)
Camera spin Z (deg)
Stand angle (deg)
// source
TSJS
/*!
* IsometricField — a DesignPass.dev component by Ernest Liu
* Docs & live playground: https://designpass.dev/components/isometric-field
* MIT licensed — keep this notice in copies and adaptations.
*/
/* IsometricField — an input rendered as an isometric slab, designed as the
companion of IsometricButton (same default camera angles). Unlike the
button, the visible top face IS the real <input>: typing, caret, and
selection must live on the rendered surface. The slab rests near the
floor and its depth edge lights up in the accent color while focused. */
.iso-field-scene {
--iso-field-rot-x: 54deg;
--iso-field-rot-z: -42deg;
/* Lean of the slab up out of the floor plane, hinged at its bottom
edge (0 = flat, -90deg = fully upright). */
--iso-field-stand: -45deg;
--iso-field-thick: 10px;
--iso-field-radius: 15px;
--iso-field-gap: 2px;
--iso-field-shift-y: 3px;
--iso-field-surface: #f4f2f8;
--iso-field-surface-focus: #ffffff;
--iso-field-edge: #b9b2c6;
--iso-field-accent: #a05cff;
--iso-field-text-color: #000000;
--iso-field-placeholder-color: rgba(0, 0, 0, 0.5);
--iso-field-font-size: 1.25rem;
--iso-field-padding-x: 20px;
position: relative;
display: block;
}
.iso-field-obj {
position: absolute;
inset: 0;
/* Isometric camera — defaults match IsometricButton so both objects can
share one 3D scene. */
transform: translateY(var(--iso-field-shift-y)) rotateX(var(--iso-field-rot-x))
rotateZ(var(--iso-field-rot-z));
transform-style: preserve-3d;
}
/* Lean the slab up around its bottom edge. */
.iso-field-stand {
position: absolute;
inset: 0;
transform-origin: center bottom;
transform: rotateX(var(--iso-field-stand));
transform-style: preserve-3d;
}
.iso-field-stand > * {
position: absolute;
inset: 0;
border-radius: var(--iso-field-radius);
transition:
transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease,
border-color 0.25s ease;
}
/* Rounded side walls, faked with stacked slices. Spread-only (no blur)
shadows fill the gaps between slices while keeping the edge crisp. */
.iso-field-side {
background: var(--iso-field-edge);
transform: translateZ(
calc(var(--iso-field-gap) + var(--iso-field-thick) * var(--i) / 10)
);
box-shadow: 0 0 0 1px var(--iso-field-edge);
transition:
transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
background-color 0.25s ease,
box-shadow 0.25s ease;
pointer-events: none;
}
/* Focused: the depth edge takes the accent color instead of a border. */
.iso-field-scene:focus-within .iso-field-side {
background: var(--iso-field-accent);
box-shadow: 0 0 0 1px var(--iso-field-accent);
}
.iso-field-top {
display: block;
width: 100%;
height: 100%;
padding: 0 var(--iso-field-padding-x);
border: 1px solid rgba(0, 0, 0, 0.12);
background: var(--iso-field-surface);
color: var(--iso-field-text-color);
font-size: var(--iso-field-font-size);
text-align: center;
outline: none;
transform: translateZ(calc(var(--iso-field-gap) + var(--iso-field-thick)));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.iso-field-top::placeholder {
color: var(--iso-field-placeholder-color);
}
.iso-field-top:focus {
background: var(--iso-field-surface-focus);
}
.iso-field-top:disabled {
cursor: not-allowed;
opacity: 0.6;
}
@media (prefers-reduced-motion: reduce) {
.iso-field-stand > * {
transition: none;
}
}
/*!
* IsometricField — a DesignPass.dev component by Ernest Liu
* Docs & live playground: https://designpass.dev/components/isometric-field
* MIT licensed — keep this notice in copies and adaptations.
*/
"use client";
import type { CSSProperties, InputHTMLAttributes } from "react";
import "./IsometricField.css";
/**
* Main knobs 90% of uses need. Everything else lives in `advanced`.
* All values map to CSS custom properties consumed by IsometricField.css;
* anything left undefined falls back to the stylesheet default.
*/
export interface IsometricFieldSettings {
/** Slab thickness, e.g. "10px". */
thickness?: string;
/** Corner radius of all faces, e.g. "15px". */
radius?: string;
/** Input surface color. */
surfaceColor?: string;
/** Depth-edge color while focused (replaces a focus ring). */
accentColor?: string;
/** Input font size, e.g. "1.25rem". */
fontSize?: string;
/** Everything else — camera, lean, colors of individual parts. */
advanced?: IsometricFieldAdvancedSettings;
}
export interface IsometricFieldAdvancedSettings {
/** Isometric camera angles — match your IsometricButton's to share a scene. */
rotateX?: string;
rotateZ?: string;
/** Lean of the slab out of the floor plane (0 = flat, "-90deg" = upright). */
standAngle?: string;
/** Height of the slab base above the floor, e.g. "2px". */
gap?: string;
/** Vertical offset of the whole scene, e.g. "3px". */
shiftY?: string;
/** Surface color while focused. */
surfaceColorFocus?: string;
/** Depth-edge color at rest. */
edgeColor?: string;
/** Input text / placeholder colors. */
textColor?: string;
placeholderColor?: string;
/** Horizontal padding inside the input, e.g. "20px". */
paddingX?: string;
}
const MAIN_VARS: Record<string, string> = {
thickness: "--iso-field-thick",
radius: "--iso-field-radius",
surfaceColor: "--iso-field-surface",
accentColor: "--iso-field-accent",
fontSize: "--iso-field-font-size",
};
const ADVANCED_VARS: Record<string, string> = {
rotateX: "--iso-field-rot-x",
rotateZ: "--iso-field-rot-z",
standAngle: "--iso-field-stand",
gap: "--iso-field-gap",
shiftY: "--iso-field-shift-y",
surfaceColorFocus: "--iso-field-surface-focus",
edgeColor: "--iso-field-edge",
textColor: "--iso-field-text-color",
placeholderColor: "--iso-field-placeholder-color",
paddingX: "--iso-field-padding-x",
};
function settingsToStyle(settings: IsometricFieldSettings): CSSProperties {
const style: Record<string, string> = {};
const { advanced, ...main } = settings;
for (const [key, value] of Object.entries(main)) {
if (value === undefined) continue;
style[MAIN_VARS[key]] = String(value);
}
for (const [key, value] of Object.entries(advanced ?? {})) {
if (value === undefined) continue;
style[ADVANCED_VARS[key]] = String(value);
}
return style as CSSProperties;
}
export interface IsometricFieldProps extends InputHTMLAttributes<HTMLInputElement> {
/** Classes applied to the outer scene wrapper — size it here (the scene
* has no intrinsic size), e.g. "w-80 h-14". */
wrapperClassName?: string;
/** Visual overrides; see IsometricFieldSettings. */
settings?: IsometricFieldSettings;
}
/** Number of stacked slices used to fake the slab's rounded side walls.
* Slices sit ~1px apart (thickness / count) with crisp, unblurred edge
* shadows — fewer, farther-apart slices needed blurred shadows to hide
* the gaps, which made the whole depth edge look fuzzy. */
const SIDE_LAYERS = 10;
/**
* An input styled as an isometric slab — the natural companion to
* IsometricButton (same default camera angles). Unlike the button, the
* real <input> is the visible top face: text entry, caret, and selection
* all need to live on the rendered surface. The slab rests near the floor
* and its depth edge lights up in the accent color while focused.
*/
export default function IsometricField({
wrapperClassName = "",
settings,
...inputProps
}: IsometricFieldProps) {
return (
<span
className={`iso-field-scene ${wrapperClassName}`}
style={settings ? settingsToStyle(settings) : undefined}
>
<span className="iso-field-obj">
<span className="iso-field-stand">
{Array.from({ length: SIDE_LAYERS }, (_, i) => (
<span
key={i}
className="iso-field-side"
style={{ "--i": i } as CSSProperties}
aria-hidden
/>
))}
<input {...inputProps} className="iso-field-top" />
</span>
</span>
</span>
);
}
// install
npx shadcn@latest add "https://designpass.dev/r/IsometricField-TS-CSS.json"Need the license details? Read the component license.