Sensible UIv1.8.0beta

The shadcn/ui look. Just write HTML.

Add one stylesheet, and your buttons, forms, tables, and dialogs look polished right away. Add a few readable classes for cards and badges, and theme everything with CSS variables. No React, Tailwind, or build step.

Inspired by shadcn/ui. Sensible UI is an independent project and is not affiliated with or endorsed by shadcn/ui.

Create your account

Start a free 14-day trial.

Team members
Name Role Status
Margaret Nguyen Owner Active
Hoshi Nakamura Editor Invited
Samuel Okafor Viewer Paused

Notifications

Choose what reaches your inbox.

Questions

Do I need React or Tailwind?

No. Add one stylesheet and write HTML.

Do I need JavaScript?

Not for styles. Accordions use native details, and a dialog needs one line to open.

Can I match my brand?

Yes. Override CSS variables such as --primary and --radius.

Release 2.4

12 changes passed review.

Invite a teammate

They will get an email with a link to join.

Everything above is plain HTML styled by Sensible UI. Its only classes are card, badge, item, and stack.

Write the HTML you already know

This sign-in card uses one class, card, and one attribute, data-variant="outline". The headings, labels, inputs, and buttons get their styles because they are HTML.

Sign in

Use the email linked to your account.

Why Sensible UI

Native elements look right

Buttons, inputs, selects, checkboxes, switches, tables, dialogs, and accordions are styled without classes.

A few readable classes

Add card, badge, or item when HTML has no element for a pattern. Choose a style with data-variant.

Familiar theme tokens

Change --primary, --radius, and other shadcn/ui-style variables. Light and dark themes are included.

Fits your project

Use a CDN link or a bundler. The scoped stylesheet styles one part of an existing app, and an optional web component adds highlighted code.

Getting started

Pick the setup that matches your project. Every option gives you the same styles.

Add it to an HTML page

Paste the stylesheet link into your page’s head. You don’t need npm or JavaScript.

Serve your page over HTTP, including when you test locally. The link pins version 1.8.0; change the version when you want to update.

Write HTML

Native elements don’t need classes. Headings, text, forms, and buttons get their styles from the elements themselves.

Get in touch

Send a message and we will reply by email.

To add a component, copy an example below. A few patterns that HTML can’t name use a class, such as class="card". Choose a style with an attribute, such as data-variant="outline". To change colors and corners, see Theming.

Style part of an existing app

If your app already has its own styles, use the scoped stylesheet. It styles only the markup inside a .sensible-ui wrapper. See Scoped adoption.

Use a bundler

Install @faith-tools/sensible-ui, then import the complete stylesheet in your CSS:

Standalone component imports include the theme and base styles they need. When markup combines components, import each component. For example, <div data-slot="table-container" class="card"> needs both /table and /card.

All public stylesheet exports
ExportPurpose
., /cssComplete bundled stylesheet
/minMinified complete bundle
/indexSource CSS entry point
/baseTheme tokens and semantic reset
/themeTheme tokens and cascade layer order
/accordion, /badge, /button, /cardStandalone component modules
/code/cssOptional code component styles; JavaScript is under /code
/description-list, /dialog, /image, /input, /itemStandalone component modules
/progress, /spinner, /table, /typographyStandalone component modules
/button-group, /input-group, /navigation, /tooltipStandalone custom element styles
/input-group/js, /tooltip/jsOptional custom element behavior
/utilsNamed layouts, container, and composition helpers
/utilities, /utilities/minOptional generated atomic utilities
/view-transitionOptional same-origin page transitions

Theming

Sensible UI uses shadcn/ui-style theme tokens, such as --background, --primary, --muted, --border, and --radius. Colors come in pairs: --primary colors a surface, and --primary-foreground colors the text on it.

Override tokens after the Sensible UI stylesheet. Put dark theme values on .dark.

Find every light and dark token in theme.css. To change fonts, override --font-sans and --font-mono.

Dark mode

Add class="dark" to an ancestor to select the bundled dark theme. Components use the same markup in both themes. System preference behavior is not enabled by the core bundle.

Dark card

The same semantic markup works.

Dark theme

Limit styles to part of a page

Use the scoped bundle when adding Sensible UI to an existing application. Semantic defaults apply only inside a neutral .sensible-ui wrapper, leaving the rest of the page alone.

If you use a bundler, import the scoped bundle in your CSS instead of using the CDN link:

Put the class on a wrapper, not on a card, table, link, or form control. Sensible UI layout and utility classes such as .stack, .mt-4, and .size-8 also belong on descendants, not the scope root. You can combine the scope class with a host-owned wrapper class. Scoped component and utility imports use the same names under /scoped, such as /scoped/button and /scoped/utilities. Scoped mode requires browser support for @scope.

View the scoped bundle beside host styles.

Typography

Standalone import: @import '@faith-tools/sensible-ui/typography';

Headings, paragraphs, links, lists, code, keyboard input, quotations, and inline text semantics are styled without classes. Keep the native element that matches the content’s meaning.

Geist and Geist Mono are optional. The stylesheet prefers them but does not load them, so your system fonts are used unless you provide the fonts separately. See the font setup in the README for HTML and Next.js examples, or override --font-sans and --font-mono to use your own fonts.

A clear heading

Use semantic HTML for meaning and emphasis.

Good defaults should support the content.

Press Ctrl + K to search.

const greeting = "Hello";

Buttons

Standalone import: @import '@faith-tools/sensible-ui/button';

Native buttons, button-like inputs, and a.button are supported. Use data-variant for primary, secondary, outline, ghost, link, or destructive treatment. Use data-size for sm, lg, or icon. Native disabled, aria-pressed, aria-busy, and aria-invalid attributes style state.

Use a button for actions and a link for navigation. Give icon-only buttons an accessible name. Enabled buttons scale down slightly while pressed; reduced-motion preferences disable this movement.

Buttons use touch-action: manipulation to prevent double-tap zoom while allowing scrolling and pinch zoom. The same default applies to links with href, button-like inputs, checkboxes, radios, selects, and disclosure summaries. Text fields and page containers keep their default touch behavior.

Link as button

Form controls

Standalone import: @import '@faith-tools/sensible-ui/input';

The input module styles native controls and their labels. Associate every control with a label. Use fieldset andlegend for related choices, and use aria-describedby when an error or hint needs to be announced.

Text-like inputs and states

Text, email, password, number, search, URL, and similar inputs share the same semantic default.

Enter a valid email address.

Textarea

Select

Use the native select when choosing one option from a list. Group long option lists with optgroup.

Checkbox

Use checkboxes for independent choices. The checked and disabled states are native.

Notifications

Radio group

Radio buttons with the same name represent one choice. Wrap the group in a fieldset with a legend.

Contact preference

Switch

Add role="switch" to a checkbox only when the control immediately turns a setting on or off.

Range

Provide a visible label and meaningful minimum, maximum, and initial values.

Date and time

Date and datetime-local inputs preserve each browser’s native picker and keyboard behavior.

Color

File

Use the accept attribute as a picker hint, not as file validation.

Image

Standalone import: @import '@faith-tools/sensible-ui/image';

Images receive responsive sizing and rounded corners. Use an empty alt value for decorative images and useful alternative text for informative images. Pair an image and caption with figure and figcaption.

A mountain ridge beneath a cloudy sky
Images and captions receive sensible defaults.

Accordion

Standalone import: @import '@faith-tools/sensible-ui/accordion';

Native details and summary provide disclosure behavior without JavaScript. The open attribute sets the initial state. Give related details elements the same name when only one should remain open.

Does this require JavaScript?

No. It uses the native details and summary elements.

Can only one item stay open?

Yes. Give related details elements the same name.

Dialog

Standalone import: @import '@faith-tools/sensible-ui/dialog';

A native dialog opens modally with showModal() or non-modally with show(). The standalone dialog import includes button styles; import /input separately for form controls. JavaScript only connects your trigger to the native method; no component runtime is required. Do not set open directly to create a modal.

Identify the dialog with aria-labelledby and add aria-describedby for a short description. Direct header, section, and footer children provide the title, scrollable content, and actions. Provide a visible close control. A form method="dialog" closes without submitting data; handle saving in your application and close only after it succeeds. Choose initial focus with autofocus when needed.

Project details

A quick look at your project.

Website redesign is ready for review.

Optional backdrop dismissal

Modal dialogs close with Escape by default, but a backdrop click does not dismiss them. Add closedby="any" only when discarding the dialog is safe. Keep a visible close control for browsers that do not support closedby.

A quick tip

Click outside this dialog, press Escape, or select Close.

Long content

The content region scrolls while the header and actions stay visible. On short viewports, the whole dialog scrolls so the body remains readable. The same layout works in dark mode and at narrow widths.

Project notes

Review the notes before continuing.

Start with the goals and the people the project serves.

Check the scope, timeline, and remaining questions.

Record feedback from the team and assign follow-up work.

Review the revised design with stakeholders.

Confirm the final content and accessibility checks.

Prepare the handoff notes and the release checklist.

Keep decisions and open questions together for reference.

Non-modal

show() leaves the page interactive and has no backdrop or automatic Escape dismissal. Always include an explicit close control. Use a modal when the task requires focus to stay inside the dialog.

Quick note

You can still use the rest of the page.

Description list

Standalone import: @import '@faith-tools/sensible-ui/description-list';

Use a description list for name-value groups, terms and definitions, or metadata. Multiple terms may share a description and one term may have multiple descriptions. Add class="card" and import the card module for the bordered treatment.

Status
Active
Plan
Team
Renewal date

Table

Standalone import: @import '@faith-tools/sensible-ui/table';

Use tables for two-dimensional data. Add a caption when the surrounding context does not already identify the table, and usescope on row and column headers. Wrap the table in data-slot="table-container" so wide content scrolls horizontally instead of overflowing the page. Body cells wrap before the table scrolls, the caption stays in view, and a fade marks the edge with more columns. Give the container role="region", tabindex="0", and aria-labelledby pointing at the caption so keyboard users can scroll it. Add class="card" to the container and import the card module for the bordered treatment.

Current project members
Name Role Status
Margaret Nguyen Owner Active
Hoshi Nakamura Editor Invited
Total2

Card

Standalone import: @import '@faith-tools/sensible-ui/card';

Add class="card" to a semantic container. Direct header, section, and footer children define its regions. Add data-slot="card-action" to a header action. Cards adapt their layout through container queries.

Team plan

For growing organizations

Invite collaborators and share project settings.

Badge

Standalone import: @import '@faith-tools/sensible-ui/badge';

Add class="badge" to short status or category text. Supported variants are primary, secondary, outline, and destructive. Use a link only when the badge navigates somewhere. aria-invalid="true" provides the invalid state.

Default Secondary Outline Destructive Linked badge

Item

Standalone import: @import '@faith-tools/sensible-ui/item';

An item is a compact content row with an optional icon or action. Use a.item when the entire row navigates. Do not put another interactive control inside a linked item.

Project settings

Manage members, billing, and notifications.

Linked item

The whole row is one descriptive link.

Loading spinner

Standalone import: @import '@faith-tools/sensible-ui/spinner';

Add aria-busy="true" while an element is updating. Add data-variant="overlay" to dim existing children. Keep visible loading text or an accessible name so the state is understandable without relying on motion.

Loading report

The current content remains visible while loading.

Progress

Standalone import: @import '@faith-tools/sensible-ui/progress';

Use the native progress element without a class. Set value and max when you know how much work is done. Leave out the value attribute entirely for indeterminate work, and a segment moves along the track; browsers disagree about an empty value="". With reduced motion, indeterminate progress shows still stripes instead.

Give each bar a visible label. When people need the number, show it as text inside the label with aria-hidden="true"; screen readers already announce the bar's value. Change the color with --primary, and pick one dark enough to stand out from its lighter track. The bar is a block, so put it after text rather than inside a sentence. In a row with other content, the bar shrinks to make room but keeps at least 4rem. Use aria-busy="true" on the region that is updating, not on the progress bar.

60%
Syncing

Button group

Standalone import: @import '@faith-tools/sensible-ui/button-group';

Wrap related controls in <sensible-button-group> to join their borders and corners. It works without JavaScript. Put native buttons, a.button links, inputs, selects, or text in a span directly inside it. Add orientation="vertical" to stack the controls. Use an hr to separate buttons that have no border, and nest groups to put space between them.

A row that is too long for its container wraps onto more rows, and text inputs keep at least 8rem. Wrapped rows lose the joined look: corners at each break stay square, and a row that starts mid-group has no border on its start edge. When a set may not fit your narrowest screen, split it into nested groups, which wrap as whole groups, or use orientation="vertical". Corners follow the nearest dir attribute, up to three nested levels, so set dir="rtl" on the page or the group rather than relying on dir="auto" or the CSS direction property. A <sensible-tooltip> around a control joins like the control itself. Other wrappers break the join, so put a checkbox inside a label, and point a button at a form with its form attribute instead of wrapping it in the form. Use one button size per group.

The element only changes how controls look. When the controls act on the same thing, add role="group" and an aria-label so screen readers announce the group. Every control stays in the tab order. Mark toggles with aria-pressed, and give icon-only buttons an aria-label. Do not use role="toolbar" unless you also add arrow-key navigation; this element does not. Inputs still need an accessible name, because text such as https:// beside an input is not its label.



https://

Input group

Standalone import: @import '@faith-tools/sensible-ui/input-group';

Wrap a text input or textarea in <sensible-input-group> to put icons, text, and buttons inside the same border. The group shows one focus ring when the field has keyboard focus, and it takes the field's aria-invalid and disabled states. Put icons, span text, and native buttons directly inside it. With a text area, put rows of text or actions in a div after it. The text area grows with its content up to 16rem, then scrolls.

When space runs out, text after the field shortens with an ellipsis before the field gets narrower than 4rem, and button labels wrap. Prefixes such as a currency, and counts in an output, stay whole until they reach half the group. The group stays on one line, so keep its addons to what fits on your narrowest screen; move a count to help text and secondary actions beside or below the field when they do not fit. Put a long fixed domain in help text below the field instead of a suffix. A select inside the group is not supported; use a button group for a select beside a field.

The examples use inline SVG icons from Lucide; any inline SVG works. Hide decorative icons with aria-hidden="true". An icon or prefix is not a label, so give the field a label or an aria-label, and give icon-only buttons an aria-label. Buttons stay separate tab stops after the field.

Styles work without JavaScript. The optional script makes a click on an icon, text, or empty space focus the field, as it would on a plain input:

With a bundler, import @faith-tools/sensible-ui/input-group/js in your browser JavaScript.

12 results
https:// .com
@ That username is taken.
Shift + Enter for a new line

Tooltip

Standalone import: @import '@faith-tools/sensible-ui/tooltip';

A tooltip shows a short description when people hover over, focus, or press and hold a control. Wrap the control and a role="tooltip" element in <sensible-tooltip>, with the control first. Set side to top (the default), bottom, left, or right; other values show it on top. The tooltip flips to the other side when it would leave the screen, and a left or right tooltip moves above or below its control when neither side has room. The wrapper adds no box, so a wrapped field keeps its width and its label spacing.

Showing the tooltip needs the optional script. Load it as a module, or import @faith-tools/sensible-ui/tooltip/js with a bundler:

The script links the tooltip to the control with aria-describedby, so screen readers announce it as a description. In browsers that support interest invokers, it adds interestfor and lets the browser handle hover, focus, long press, and Esc. Elsewhere, it shows the tooltip after a short hover delay, on keyboard focus, and after a half-second touch hold. People can move the pointer onto the tooltip, and Esc closes it. It also closes once its control scrolls out of view, so it never floats away from it. Touch behavior is tested with emulated touch input, not yet on real phones or tablets. If the browser's anchor positioning leaves a tooltip away from its control, as Firefox does in a right-to-left part of a left-to-right page, the script places it beside the control. Only one tooltip shows at a time. Esc that closes a tooltip has defaultPrevented set, so check it before your own Esc shortcuts act. In both cases, once a tooltip shows, the next tooltip shows without the delay while people move between controls. The delay returns 300ms after the last tooltip closes, or right after Esc. Without the script, the text stays hidden on screen but remains next to the control for assistive technology.

Keep tooltips short and never put links or buttons in them. Do not put essential information only in a tooltip, because many touch users will not find it. The control needs its own accessible name. When the tooltip is the name of an icon button, give the tooltip an id and point aria-labelledby at it instead. Disabled buttons cannot receive focus or hover, so a tooltip on one never shows; explain why a control is unavailable in visible text.

Add to library Save draft Ctrl S Bold Pricing details Prices include tax

Container

Standalone import: @import '@faith-tools/sensible-ui/utils';

Add class="container" to center content and limit its width. It fills the available width up to 640px by default, with 1rem of padding on all sides. At viewport widths of 40rem and above, the padding becomes 1.5rem. The core bundle includes this class; no companion stylesheet is needed.

Set --container-width on the container to choose a different maximum width. The example uses 32rem. The class works on main, section, or div; a plain main does not get container styles automatically. .page is an alias for .container.

Centered content

Keep content readable without stretching across the whole page.

Named layouts

Standalone import: @import '@faith-tools/sensible-ui/utils';

The core bundle includes .stack for vertical flow, .cluster for wrapping inline groups, .split for separated content, and .auto-grid for intrinsic grids. .y-stack aliases stack and .x-stack is a non-wrapping inline stack. Set --layout-gap to adjust spacing and --min-item-size to control grid wrapping.

Cluster Wraps inline content
Split layout
First
Second
Third

Optional atomic utilities

Optional import: @import '@faith-tools/sensible-ui/utilities';

The companion stylesheet provides token-backed display, flex, grid, alignment, sizing, spacing, positioning, overflow, text, aspect-ratio, and accessibility helpers. It is plain generated CSS and requires no template scanning or consumer-side tooling. Override the --space-* custom properties to change its spacing scale. Prefer named helpers such as .stack, .y-stack, and .x-stack for common composition, then use atomic helpers for exceptions. Breakpoint-prefixed variants are intentionally not generated; use intrinsic layouts or consumer-owned media queries instead.

1:1
Status: Utility example

A long status message is truncated with an ellipsis when the available inline space is limited, preserving a compact row without wrapping into the content below or pushing neighboring controls out of view.

Add highlighted code

Add the optional stylesheet and browser module to pages that need syntax highlighting. The component works without the main Sensible UI stylesheet. Serve the page over HTTP so the browser can load the module.

  1. Add the component styles to your HTML:

  2. Load the component as a browser module:

  3. Add sensible-code with a read-only textarea containing the source:

If you use a bundler, install @faith-tools/sensible-ui. Import the styles in your CSS and the component in your browser JavaScript instead of using the CDN tags:

The code below shows syntax colors, a Wrap lines toggle, and a Copy button. Code scrolls horizontally by default at every screen width. Set data-wrap="true" to start a block wrapped. Readers can switch either block between wrapping and scrolling. Without JavaScript, the read-only text area remains readable. The component creates pre and code when it loads. In HTML source, escape & before entity names. If the example contains </textarea>, write its opening angle bracket as &lt;. Set language to a supported language name, such as html, css, or python.