Forms

Input

A text field that always has a label, with optional hint and error text.

Installation

npx sinaui add input

Examples

Basic

<script lang="ts">
	import Input from '#lib/ui/Input.svelte';
</script>

<Input label="Title of the work" placeholder="Kitab al-Manazir" />

With a hint

The hint is read out after the label, so instructions reach screen reader users too.

As it appears in the House of Wisdom register.

<script lang="ts">
	import Input from '#lib/ui/Input.svelte';
</script>

<Input
	label="Full name"
	placeholder="Abu Ali al-Husayn ibn Sina"
	autocomplete="name"
	hint="As it appears in the House of Wisdom register."
/>

With an error

An error marks the field invalid and is read out with it. Type a full address to clear it.

Enter an email like khwarizmi@baytalhikma.org.

<script lang="ts">
	import Input from '#lib/ui/Input.svelte';

	let email = $state('khwarizmi@');
	const error = $derived(
		/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)
			? undefined
			: 'Enter an email like khwarizmi@baytalhikma.org.'
	);
</script>

<Input label="Email" type="email" autocomplete="email" bind:value={email} {error} />

Disabled

<script lang="ts">
	import Input from '#lib/ui/Input.svelte';
</script>

<Input label="Library" value="Bayt al-Hikma, Baghdad" disabled />

With a prefix and suffix

Text or an icon inside the box, before or after what's typed.

https:// .org
<script lang="ts">
	import Input from '#lib/ui/Input.svelte';
</script>

<Input label="Library website" placeholder="bayt-al-hikma">
	{#snippet start()}https://{/snippet}
	{#snippet end()}.org{/snippet}
</Input>

With a button

A button inside the box: here, copying the link, with a tick to say it worked.

<script lang="ts">
	import { Copy01Icon, Tick02Icon } from '@hugeicons/core-free-icons';
	import Input from '#lib/ui/Input.svelte';
	import InputButton from '#lib/ui/InputButton.svelte';
	import { announce } from '#lib/ui/announce.js';

	const link = 'https://bayt-al-hikma.org/manuscripts/optics-1021';
	let copied = $state(false);

	async function copy() {
		await navigator.clipboard.writeText(link);
		copied = true;
		announce('Link copied');
		setTimeout(() => (copied = false), 1600);
	}
</script>

<Input label="Share link" value={link} readonly onfocus={(e) => e.currentTarget.select()}>
	{#snippet end()}
		<InputButton icon={copied ? Tick02Icon : Copy01Icon} aria-label="Copy link" onclick={copy} />
	{/snippet}
</Input>

Inline edit

Text that turns into a field where it stands: for titles, names and settings shown as a page rather than a form. Press it (or Enter) to edit; Enter or moving away saves, Escape puts it back.

npx sinaui add inline-edit

A title

Hover to see the pencil. Clearing it and saving says a title is needed.

Title
<script lang="ts">
	import InlineEdit from '#lib/ui/InlineEdit.svelte';

	let title = $state('Kitab al-Manazir (Book of Optics)');
</script>

<div class="demo">
	<InlineEdit label="Title" bind:value={title} required maxlength={80} />
</div>

<style>
	.demo {
		inline-size: min(24rem, 100%);
	}
</style>

A profile, edited in place

Each field saves on its own. The email is checked before saving; the city fails once, to show the message and a second try.

Name
Name
Email
Email
City
City
Note
Note
<script lang="ts">
	import InlineEdit from '#lib/ui/InlineEdit.svelte';

	let name = $state('Maryam al-Asturlabi');
	let email = $state('maryam@bayt-al-hikma.org');
	let city = $state('Aleppo');
	let note = $state('');
	let tries = 0;

	// Stand-ins for your API. The city save fails the first time, to show what happens.
	const wait = () => new Promise((r) => setTimeout(r, 600));
	const saveCity = async () => {
		await wait();
		if (tries++ === 0) throw new Error('offline');
	};
</script>

<dl class="profile">
	<div>
		<dt class="sr">Name</dt>
		<dd><InlineEdit label="Name" bind:value={name} required onsave={wait} /></dd>
	</div>
	<div>
		<dt class="sr">Email</dt>
		<dd>
			<InlineEdit
				label="Email"
				type="email"
				bind:value={email}
				required
				validate={(v) =>
					/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
						? undefined
						: 'Enter an email like name@example.com.'}
				onsave={wait}
			/>
		</dd>
	</div>
	<div>
		<dt class="sr">City</dt>
		<dd><InlineEdit label="City" bind:value={city} onsave={saveCity} /></dd>
	</div>
	<div>
		<dt class="sr">Note</dt>
		<dd><InlineEdit label="Note" bind:value={note} placeholder="Add a note" onsave={wait} /></dd>
	</div>
</dl>

<style>
	.profile {
		display: grid;
		gap: 0.75rem;
		inline-size: min(24rem, 100%);
		margin: 0;
	}
	dd {
		margin: 0;
	}
	.sr {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		overflow: hidden;
		clip-path: inset(50%);
	}
</style>
Inline edit propTypeDefault
labelstring (required)
valuestring (bindable)''
placeholderstring: shown when empty'Empty'
hideLabelbooleanfalse
type'text' | 'email' | 'url''text'
requiredbooleanfalse
maxlengthnumber
validate(value) => string | undefined: a message keeps it open
onsave(value) => Promise | void: throw to keep it open with "Couldn’t save"

Props

PropTypeDefault
labelstring (required)
valuestring | number | null, bindable
start, endSnippet: inside the box
hintstring
errorstring
…restAny <input> attribute.

Accessibility

  • label is required and linked to the field: clicking it focuses the field, and screen readers announce it.
  • hint and error are joined with aria-describedby; an error also sets aria-invalid.
  • Text is at least 16px so iOS Safari doesn't zoom the page on focus.
  • Inline edit is a button named with its label, its value and "edit"; after saving or canceling, focus returns to it and "Saved" is announced.
  • Its error is joined to the field and read out; the field stays open until it's fixed.
  • Save and Cancel are real buttons, reachable with Tab.
Introduction
Theming
Changelog
Button
Action button
Confirm button
Copy button
Hold to confirm
Split button
Calendar
Checkbox
Color picker
Combobox
Date picker
Date range picker
File drop
Form
Input
Mention input
Multi select
Number field
OTP input
Password input
Phone input
Radio cards
Radio group
Rich text editor
Search field
Segmented control
Select
Signature pad
Slider
Switch
Tag input
Upload
Wheel picker
Textarea
Time field
Toggle group
Toolbar
Alert dialog
Command palette
Context menu
Dialog
Drawer
Dropdown
Hover card
Popover
Tooltip
Announcement bar
Alert
Empty state
Meter
Progress
Rolling number
Skeleton
Spinner
Toast
Avatar
Avatar group
Badge
Card
Carousel
Chart
Chat
Code block
Data table
Folder
Kbd
Mind map
Resizable panels
Scroll area
Separator
Sortable
Table
Tag group
Virtual list
Audio player
Image
Image compare
Video
Visualizer
Breadcrumb
File tree
Menubar
Navigation menu
Pagination
Sidebar layout
Stepper
Tabs
Alarm clock
Clock
Dynamic island
Fluid orb
Hourglass
Stopwatch
Timer
AI chat
AI media
Auth
Board
Booking
Dashboard
File explorer
Messaging
Notifications
Settings
Team
Overview
Pricing
Agent chat
Calendar
Voice agent
Mail inbox
CMS
Blog
Marketing site
E-commerce
AI agents
SaaS landing page
SaaS app
Accordion
Dark theme
Light theme