@andrilla/mado-ui
v1.1.2
Published
窓 UI is a collection of opinionated React 19 components built on Tailwind CSS 4.
Downloads
826
Readme
窓 (mado) UI
窓 UI is a collection of opinionated React 19 components built on Tailwind CSS 4. It was originally created for use on websites built by Andrilla, but is now available for anyone.
All of the components are built in similar ways, that always allow for customization.
How to Use
Just import the components you want to use, and use them.
Next.js
Use '@andrilla/mado-ui/client'
Install
Install or copy and paste the components you with to use.
npm i @andrilla/mado-uipnpm add @andrilla/mado-uiyarn add @andrilla/mado-uibun add @andrilla/mado-uideno add npm:@andrilla/mado-uiAdd CSS
Import CSS above your tailwind import in your globals.css.
@import 'tailwindcss';
@import '@andrilla/mado-ui/css';It's important to import tailwindcss before @andrilla/mado-ui/css, or you can just import @andrilla/mado-ui/tailwindcss which has tailwindcss already imported.
Components
Button
A pre-styled button component built on Headless UI's Button. with utility props for easy customization.
Doubles as an HTMLButtonElement and HTMLAnchorElement.
Default export:
- Button
Props include padding, rounded, theme. Just add href to make it an HTMLAnchorElement.
Example
<Button theme="orange" padding="md" rounded="lg">
Click me
</Button>
<Button href="/about" theme="blue-800" gradient>
Link Button
</Button>Form
Just a form component with className="grid gap-3".
Example
<Form>
<Input
name='email'
label='Email'
placeholder='[email protected]'
type='email'
/>
<SubmitButton>Submit</SubmitButton>
</Form>Ghost
A loading placeholder component with animated pulse effect.
Example
<Ghost />Heading
A heading component that renders HTML heading elements (h1-h6) with appropriate
styling. Automatically generates IDs for targeting, similar to a markdown. Defaults to h2.
Example
<Heading as='h1'>Work Hard and Reap the Rewards</Heading>
<Heading>Some times things aren't as they seem.</Heading>Input
A form input component built on Headless UI's Input wrapped in Headless UI's Field with built-in validation CSS, label (Headless UI Label), description (Headless UI Description), and children for custom content like tooltips. Required by default.
Example
<Input name='first_name' label='First Name' placeholder='First Name' />Textarea
Same setup as Input, but for textarea built on Headless UI's Textarea. Includes resize prop for controlling resize behavior (because the CSS can be confusing). Required by default.
Example
<Textarea name='message' label='Message' placeholder='Your message…' />Checkbox
Same setup as Input, but for checkbox built on Headless UI's Checkbox.
Example
<Checkbox name='terms' label='I agree to the terms and conditions' />Search
Same setup as Input, but for select built on Headless UI's Combobox. Uses uncontrolled state by default. Works great with multiple prop for multi-select. Includes an allowCustom prop to provide users with the option to write in their own options.
SearchOption
Add options to your Search component built on Headless UI's ComboboxOption. Designed to have customizable children, to display unique option values (e.g. icons, descriptions, etc.), while displaying just the option name in the select box.
Example
<Search name='faq' label='Search our FAQ'>
<SearchOption name='Does it work?' value='does_it_work'>
Does it work?
</SearchOption>
<SearchOption name='Can I use it?' value='can_i_use_it'>
Can I use it?
</SearchOption>
</Search>Select
Same setup as Input, but for select built on Headless UI's Listbox. Uses uncontrolled state by default. Works great with multiple prop for multi-select.
SelectOption
Add options to your Select component built on Headless UI's ListboxOption. Designed to have customizable children, to display unique option values (e.g. icons, descriptions, etc.), while displaying just the option name in the select box.
Example
<Select name='good_design' label='Do you like good design?'>
<SelectOption name='YES!' value='yes'>
<ThumbsUp />
YES!
</SelectOption>
<SelectOption name='Nah' value='no'>
<ThumbsDown />
Nah. Bad design is better.
</SelectOption>
</Select>Fieldset
A form fieldset component built on Headless UI's Fieldset with built-in legend (Headless UI Legend).
Example
<Fieldset legend='Legend'>
<Input name='first_name' label='First Name' placeholder='First Name' />
<Input name='last_name' label='Last Name' placeholder='Last Name' />
</Fieldset>Anchor
A link component that removes hashes after scrolling to anchor points, and adds a scale down on click. Generally you should use the Link component, but this is good for wrapping images or other non-text content. Don't forget to make it accessible by adding a title attribute, an sr-only span, or aria-label.
Example
<Anchor href='#section' title='go somewhere else'>
<svg viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='50' />
</svg>
</Anchor>Link
A link component with various animation styles and theming options. Supports both single-line and multiline text.
Example
<Link href="/about" lineType="ltr" theme="mauve">
Learn more
</Link>
<Link href="/contact" lineType="multiline-fill-center" theme="cyan">
Get in touch
</Link>Modal
A pre-designed modal component built on Headless UI's Dialog. Handles transitions, drag-to-close functionality, and backdrop interactions.
Example
<Modal>
<ModalTrigger>Open Modal</ModalTrigger>
<ModalDialog>
<ModalTitle>Modal Content</ModalTitle>
<ModalDescription>This is the modal content.</ModalDescription>
</ModalDialog>
</Modal>SubmitButton
A specialized button for form submission with status-aware styling and content.
Example
<SubmitButton
formStatus={formStatus}
success='Form Submitted!'
error='Submission Failed'
>
Submit
</SubmitButton>Human Verification
A bot-preventing submission method, forcing an iOS 5 like 'swipe to verify' gesture. This can be used in place of a submit button or as an additional layer of security.
This is still in beta, due to potential accessibility issues, despite included keyboard support. It also includes token generation and verification, although additional server-side verification API is not supported yet.
Example
<Form>
<HumanVerification />
</Form>Details
A pre-designed extension of Headless UI's Disclosure.
DetailsSummary
A pre-designed extension of Headless UI's DisclosureButton, that utilizes the Mado UI Button component.
DetailsBody
A pre-designed extension of Headless UI's DisclosurePanel.
Example
<Details>
<DetailsSummary>What is an American Cardinal?</DetailsSummary>
<DetailsBody>
An American Cardinal is a bird. Male cardinals have a bright red breast and
a black face mask. Female cardinals have a duller brown coloration. This
allows the male cardinals to draw attention to themselves and away from
their mates, as their mates blend in with the trees.
</DetailsBody>
</Details>DropDown
A pre-designed extension of Headless UI's Menu.
DropDownButton
An extension of Headless UI's MenuButton.
DropDownItems
A pre-designed extension of Headless UI's MenuItems.
DropDownItem
An extension of Headless UI's MenuItem.
DropDownSection
An extension of Headless UI's MenuSection with a built-in label prop that utilizes a pre-designed extension of Headless UI's MenuHeading and optional separatorAbove and separatorBelow props to automatically add separators around the section with DropDownSeparator.
DropDownSeparator
An pre-designed extension of Headless UI's MenuSeparator.
Example
<DropDown>
<DropDownButton>Actions</DropDownButton>
<DropDownItems>
<DropDownSection label='Edit'>
<DropDownItem>Edit</DropDownItem>
<DropDownItem>Copy</DropDownItem>
<DropDownItem>Paste</DropDownItem>
</DropDownSection>
<DropDownSection label='Share' separatorAbove>
<DropDownItem>Share</DropDownItem>
<DropDownItem>Download</DropDownItem>
</DropDownSection>
<DropDownSeparator />
<DropDownSection label='Help'>
<DropDownItem>Quick Tips</DropDownItem>
<DropDownItem>Docs</DropDownItem>
</DropDownSection>
</DropDownItems>
</DropDown>Tooltip
A tooltip component that displays a tooltip on hover or focus.
TooltipTrigger
The trigger element that opens the tooltip on hover or focus.
TooltipDisplay
The element that is the tooltip content.
Example
<Tooltip>
<TooltipTrigger>More details</TooltipTrigger>
<TooltipDisplay>
This is helpful for showing additional information about the trigger
element.
</TooltipDisplay>
</Tooltip>IFrame
An iframe component with better built-in type safety and required title prop for accessibility and SEO.
Example
<IFrame src='https://andrilla.net' title={`Andrilla's website`} />Time
A time component that displays formatted dates and times with customizable precision.
Example
<Time dateObject={new Date()} day month year hours minutes />
<Time dateTime='2024-01-01T12:00:00Z' />Video
It is a video component with some nice built-in features, but it's still better to go with Vimeo. The progressive enhancement needs some work before it's ready for production.
CSS Changes
To help create better design with less work, there are some changes to the Tailwind base that you should be aware of. There are also some additions to the theme and some helpful utility classes that may prove useful.
Base
@layer base {
/* Inputs have a min-width by default which makes working with them confusing. We removed that to make them easier to work with. A relative position is generally preferred over static, since absolute positioning outside of a direct parent is a rarity, and can be easily changed with the static class. */
* {
position: relative;
min-width: 0;
}
/* Helps differentiate paragraphs from headings, and makes it pretty. */
p {
color: color-mix(in oklch, currentColor 80%, transparent);
text-wrap: pretty;
}
/* We think this is a better default for users. */
button {
cursor: pointer;
}
/* Aligns svgs without defined colors with the currentColor. */
svg {
fill: currentColor;
stroke: currentColor;
stroke-width: 0;
}
}Theme
@theme {
/* This is used for the Tooltip. It's very straightforward. */
--animate-fade-in: fade-in var(--tw-animation-duration, 0.2s)
var(--tw-animation-timing-function, cubic-bezier(0.25, 0.5, 0, 0.99))
var(--tw-animation-fill-mode, forwards);
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* This is used for the loading state of the SubmitButton. Pair this with animation-delay on adjacent elements to see the wave. */
--animate-wave: wave var(--tw-animation-duration, 3s)
var(--tw-animation-timing-function, ease-in-out)
var(--tw-animation-iteration-count, infinite);
@keyframes wave {
0%,
15%,
85%,
100% {
transform: translateY(0);
}
35% {
transform: translateY(-3px);
}
65% {
transform: translateY(3px);
}
}
/* For the added animation utilities. */
--animation-direction-normal: normal;
--animation-direction-reverse: reverse;
--animation-direction-alternate: alternate;
--animation-direction-alternate-reverse: alternate-reverse;
--animation-fill-none: none;
--animation-fill-forwards: forwards;
--animation-fill-backwards: backwards;
--animation-fill-both: both;
--animation-iteration-infinite: infinite;
--animation-range-start-contain: contain;
--animation-range-start-cover: cover;
--animation-range-start-normal: normal;
--animation-state-paused: paused;
--animation-state-running: running;
/* Most of the time the theme color of buttons and links should match your branding. Reset the --base-theme-color (and optionally the --base-theme-color--foreground) in your @theme to automatically match the default theme color to your brand, without having to reset it on each button and link manually. */
--base-theme-color: var(--color-blue-500);
--base-theme-color--foreground: color-mix(
in oklch,
var(--base-theme-color) 5%,
var(--color-white)
);
/* For the added corner-shape-* utility. */
--corner-shape-bevel: bevel;
--corner-shape-notch: notch;
--corner-shape-round: round;
--corner-shape-scoop: scoop;
--corner-shape-square: square;
--corner-shape-squircle: squircle;
--corner-shape-infinity: superellipse(infinity);
/* Exponential easing feels more responsive than other easing methods. */
--ease-exponential: cubic-bezier(0.25, 0.5, 0, 0.99);
/* Spring easing provides a bouncy feel without the need for JavaScript. */
--ease-spring: cubic-bezier(0.35, 1.67, 0.44, 0.78);
/* You may want to modify these, or the neutral colors, to fit the branding of your website. These are primarily used to assist with ring offsets. Add these to the body of your @layer base to take full advantage of them. */
--global-bg: light-dark(var(--color-neutral-50), var(--color-neutral-900));
--global-text: light-dark(var(--color-neutral-950), var(--color-neutral-100));
/* Helpful relative font sizing. */
--text-smaller: smaller;
--text-larger: larger;
}/* Animation utility classes that do what you think they do. */
@utility animation-delay-* {
--tw-animation-delay: --value([*]);
/* prettier-ignore */
--tw-animation-delay: --value(integer)ms;
animation-delay: var(--tw-animation-delay);
}
@utility animation-direction-* {
--tw-animation-direction: --value(--animation-direction-*);
animation-direction: var(--tw-animation-direction);
}
@utility animation-duration-* {
--tw-animation-duration: --value([*]);
/* prettier-ignore */
--tw-animation-duration: --value(integer)ms;
animation-duration: var(--tw-animation-duration);
}
@utility animation-ease-* {
--tw-animation-timing-function: --value(--ease-*, [*]);
animation-timing-function: var(--tw-animation-timing-function);
}
@utility animation-fill-* {
--tw-animation-fill-mode: --value(--animation-fill-*);
animation-fill-mode: var(--tw-animation-fill-mode);
}
@utility animation-iteration-* {
--tw-animation-iteration-count: --value(--animation-iteration-*, integer);
animation-iteration-count: var(--tw-animation-iteration-count);
}
@utility animation-start-* {
--tw-animation-range-start: --value(--animation-range-start-*, [*]);
/* prettier-ignore */
--tw-animation-range-start: --value(number)px;
--tw-animation-range-start: --value(percentage);
animation-range-start: var(--tw-animation-range-start);
}
@utility animation-state-* {
--tw-animation-state: --value(--animation-state-*);
animation-play-state: var(--tw-animation-state);
}
/* All various corner-shape options. Not supported in all browsers yet, but we added it to Button, so now it's future-proofed. Use `corner-super-1.5` for a superellipse that looks good with or without browser support. */
@utility corner-* {
corner-shape: --value(--corner-shape-*, [*]);
}
@utility corner-s-* {
corner-start-start-shape: --value(--corner-shape-*, [*]);
corner-end-start-shape: --value(--corner-shape-*, [*]);
}
@utility corner-e-* {
corner-start-end-shape: --value(--corner-shape-*, [*]);
corner-end-end-shape: --value(--corner-shape-*, [*]);
}
@utility corner-t-* {
corner-top-left-shape: --value(--corner-shape-*, [*]);
corner-top-right-shape: --value(--corner-shape-*, [*]);
}
@utility corner-r-* {
corner-top-right-shape: --value(--corner-shape-*, [*]);
corner-bottom-right-shape: --value(--corner-shape-*, [*]);
}
@utility corner-b-* {
corner-bottom-left-shape: --value(--corner-shape-*, [*]);
corner-bottom-right-shape: --value(--corner-shape-*, [*]);
}
@utility corner-l-* {
corner-top-left-shape: --value(--corner-shape-*, [*]);
corner-bottom-left-shape: --value(--corner-shape-*, [*]);
}
@utility corner-ss-* {
corner-start-start-shape: --value(--corner-shape-*, [*]);
}
@utility corner-se-* {
corner-start-end-shape: --value(--corner-shape-*, [*]);
}
@utility corner-es-* {
corner-end-start-shape: --value(--corner-shape-*, [*]);
}
@utility corner-tl-* {
corner-top-left-shape: --value(--corner-shape-*, [*]);
}
@utility corner-tr-* {
corner-top-right-shape: --value(--corner-shape-*, [*]);
}
@utility corner-br-* {
corner-bottom-right-shape: --value(--corner-shape-*, [*]);
}
@utility corner-bl-* {
corner-bottom-left-shape: --value(--corner-shape-*, [*]);
}
@utility -corner-infinity {
corner-shape: superellipse(-infinity);
}
@utility -corner-s-infinity {
corner-start-start-shape: superellipse(-infinity);
corner-end-start-shape: superellipse(-infinity);
}
@utility -corner-e-infinity {
corner-start-end-shape: superellipse(-infinity);
corner-end-end-shape: superellipse(-infinity);
}
@utility -corner-t-infinity {
corner-top-left-shape: superellipse(-infinity);
corner-top-right-shape: superellipse(-infinity);
}
@utility -corner-r-infinity {
corner-top-right-shape: superellipse(-infinity);
corner-bottom-right-shape: superellipse(-infinity);
}
@utility -corner-b-infinity {
corner-bottom-left-shape: superellipse(-infinity);
corner-bottom-right-shape: superellipse(-infinity);
}
@utility -corner-l-infinity {
corner-top-left-shape: superellipse(-infinity);
corner-bottom-left-shape: superellipse(-infinity);
}
@utility -corner-ss-infinity {
corner-start-start-shape: superellipse(-infinity);
}
@utility -corner-se-infinity {
corner-start-end-shape: superellipse(-infinity);
}
@utility -corner-es-infinity {
corner-end-start-shape: superellipse(-infinity);
}
@utility -corner-tl-infinity {
corner-top-left-shape: superellipse(-infinity);
}
@utility -corner-tr-infinity {
corner-top-right-shape: superellipse(-infinity);
}
@utility -corner-br-infinity {
corner-bottom-right-shape: superellipse(-infinity);
}
@utility -corner-bl-infinity {
corner-bottom-left-shape: superellipse(-infinity);
}
@utility corner-super-* {
corner-shape: superellipse(--value(number));
}
@utility corner-s-super-* {
corner-start-start-shape: superellipse(--value(number));
corner-end-start-shape: superellipse(--value(number));
}
@utility corner-e-super-* {
corner-start-end-shape: superellipse(--value(number));
corner-end-end-shape: superellipse(--value(number));
}
@utility corner-t-super-* {
corner-top-left-shape: superellipse(--value(number));
corner-top-right-shape: superellipse(--value(number));
}
@utility corner-r-super-* {
corner-top-right-shape: superellipse(--value(number));
corner-bottom-right-shape: superellipse(--value(number));
}
@utility corner-b-super-* {
corner-bottom-left-shape: superellipse(--value(number));
corner-bottom-right-shape: superellipse(--value(number));
}
@utility corner-l-super-* {
corner-top-left-shape: superellipse(--value(number));
corner-bottom-left-shape: superellipse(--value(number));
}
@utility corner-ss-super-* {
corner-start-start-shape: superellipse(--value(number));
}
@utility corner-se-super-* {
corner-start-end-shape: superellipse(--value(number));
}
@utility corner-es-super-* {
corner-end-start-shape: superellipse(--value(number));
}
@utility corner-tl-super-* {
corner-top-left-shape: superellipse(--value(number));
}
@utility corner-tr-super-* {
corner-top-right-shape: superellipse(--value(number));
}
@utility corner-br-super-* {
corner-bottom-right-shape: superellipse(--value(number));
}
@utility corner-bl-super-* {
corner-bottom-left-shape: superellipse(--value(number));
}
@utility -corner-super-* {
corner-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-s-super-* {
corner-start-start-shape: superellipse(calc(--value(number) * -1));
corner-end-start-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-e-super-* {
corner-start-end-shape: superellipse(calc(--value(number) * -1));
corner-end-end-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-t-super-* {
corner-top-left-shape: superellipse(calc(--value(number) * -1));
corner-top-right-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-r-super-* {
corner-top-right-shape: superellipse(calc(--value(number) * -1));
corner-bottom-right-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-b-super-* {
corner-bottom-left-shape: superellipse(calc(--value(number) * -1));
corner-bottom-right-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-l-super-* {
corner-top-left-shape: superellipse(calc(--value(number) * -1));
corner-bottom-left-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-ss-super-* {
corner-start-start-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-se-super-* {
corner-start-end-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-es-super-* {
corner-end-start-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-tl-super-* {
corner-top-left-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-tr-super-* {
corner-top-right-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-br-super-* {
corner-bottom-right-shape: superellipse(calc(--value(number) * -1));
}
@utility -corner-bl-super-* {
corner-bottom-left-shape: superellipse(calc(--value(number) * -1));
}
/* Makes it possible to utilize the same gap as parent elements without subgrid. */
@utility gap-* {
--tw-gap: --value([*]);
--tw-gap-x: --value([*]);
--tw-gap-y: --value([*]);
--tw-gap: calc(var(--spacing) * --value(number));
--tw-gap-x: var(--tw-gap, calc(var(--spacing) * --value(number)));
--tw-gap-y: var(--tw-gap, calc(var(--spacing) * --value(number)));
}
@utility gap-x-* {
--tw-gap-x: --value([*]);
--tw-gap-x: calc(var(--spacing) * --value(number));
}
@utility gap-y-* {
--tw-gap-y: --value([*]);
--tw-gap-y: calc(var(--spacing) * --value(number));
}
/* Makes animating width relatively possible. */
@utility grid-cols-0fr {
grid-template-columns: 0fr;
}
@utility grid-cols-1fr {
grid-template-columns: 1fr;
}
/* Makes animating height relatively possible. */
@utility grid-rows-0fr {
grid-template-rows: 0fr;
}
@utility grid-rows-1fr {
grid-template-rows: 1fr;
}
/* Makes animating on hover or other pseudo states with grid-template-columns/rows easier. Meant to pair with the previous four grid-template utilities. */
@utility transition-cols {
transition-property: grid-template-columns;
}
@utility transition-rows {
transition-property: grid-template-rows;
}Note About Theme
The theme prop includes all Tailwind CSS 4.2 colors by default, plus a 'custom' value for defining custom themes. Built-in colors use shade -500 by default, when writing theme='blue' for example. Built-in colors also have sensible defaults for foreground contrast, based on the chosen shade. 50-400 uses color-mix(in oklch, var(--theme-color) 5%, var(--color-black)), while 500-950 uses color-mix(in oklch, var(--theme-color) 5%, var(--color-white)). Using 'custom' defaults to a light foreground, but you can add the following to change to a dark foreground:
For Buttons
'text-[color-mix(in_oklch,var(--theme-color)_5%,var(--color-black))]'For Links
'[--text-color:color-mix(in_oklch,var(--theme-color)_5%,var(--color-black))]'