Info Tip
A small (i) next to a label that opens a toggletip on click or focus, with a Learn more link; works on touch.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/info-tip.json'use client'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import './info-tip.css'
export interface InfoTipProps {
children: ReactNode
label?: string
href?: string
side?: 'top' | 'bottom'
fill?: 'dark' | 'light'
className?: string
}
/** Click / focus toggletip for inline help. */
export function InfoTip({ children, label = 'More info', href, side = 'top', fill = 'dark', className }: InfoTipProps) {
const [open, setOpen] = useState(false)
const [s, setS] = useState(side)
const root = useRef<HTMLSpanElement>(null)
useEffect(() => {
if (!open) return
const r = root.current?.getBoundingClientRect()
if (r) setS(side === 'top' && r.top < 140 ? 'bottom' : side === 'bottom' && innerHeight - r.bottom < 140 ? 'top' : side)
const out = (e: PointerEvent) => { if (!root.current?.contains(e.target as Node)) setOpen(false) }
addEventListener('pointerdown', out)
return () => removeEventListener('pointerdown', out)
}, [open, side])
return (
<span ref={root} className={['sd-itp', className].filter(Boolean).join(' ')} data-fill={fill} onKeyDown={(e) => e.key === 'Escape' && setOpen(false)}>
<button type="button" className="sd-itp__b" aria-label={label} aria-expanded={open} onClick={() => setOpen(!open)}>
<svg viewBox="0 0 14 14" aria-hidden><circle cx="7" cy="7" r="5.8" /><path d="M7 6.3v3.6M7 4.2v.1" /></svg>
</button>
<span role="status" aria-live="polite">
{open && <span className="sd-itp__tip" data-side={s}>{children}{href && <><br /><a href={href} onClick={(e) => href === '#' && e.preventDefault()}>Learn more</a></>}</span>}
</span>
</span>
)
}.sd-itp { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-itp[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-itp button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-itp :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-itp .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-itp { position: relative; display: inline-flex; align-items: center; vertical-align: middle; }
.sd-itp__b { display: grid; place-items: center; width: 20px; height: 20px; margin-left: 4px; border-radius: 99px; color: var(--mute); transition: color 150ms, background-color 150ms; }
.sd-itp__b:hover, .sd-itp__b[aria-expanded='true'] { color: var(--fg); background: var(--soft) !important; }
.sd-itp__b svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
.sd-itp__tip { position: absolute; left: 50%; width: 230px; padding: 12px; border-radius: 12px; background: var(--strong); color: var(--on); font-size: 12.5px; line-height: 1.45; text-align: left; transform: translateX(-50%); z-index: 5; animation: sd-itp-in 160ms cubic-bezier(0.2, 1.2, 0.3, 1); }
.sd-itp__tip[data-side='top'] { bottom: calc(100% + 8px); }
.sd-itp__tip[data-side='bottom'] { top: calc(100% + 8px); }
.sd-itp__tip a { display: inline-block; margin-top: 6px; color: var(--on); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
@keyframes sd-itp-in { from { opacity: 0; transform: translateX(-50%) scale(0.95); } }
.sd-itp__demo { width: min(100%, 300px); padding: 16px; border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); color: var(--fg); }
.sd-itp__demo .k { font-size: 12.5px; color: var(--mute); display: flex; align-items: center; }
.sd-itp__demo .v { font-size: 28px; font-weight: 650; letter-spacing: -0.02em; margin-top: 2px; }
@media (prefers-reduced-motion: reduce) { .sd-itp *, .sd-itp *::before, .sd-itp *::after { animation: none !important; transition: none !important; } }Overview
Unlike hover-only tooltips, this opens on click and on keyboard focus, so it works on phones and can hold a link. It positions above or below and closes on Escape or outside click.
The content is announced through a live region when opened, the accessible toggletip pattern.
When to use
- Explaining a field, metric or setting in place.
When not to use
- Essential instructions: put them in the UI.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/info-tip.json$
npx shadcn@latest add https://sundaeui.dev/r/info-tip.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/info-tip.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/info-tip.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy info-tip.tsx and info-tip.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { InfoTip } from '@/components/sundae/info-tip'
<InfoTip />Props
| Prop | Type | Default |
|---|---|---|
children*Tip content. | ReactNode | — |
labelAccessible name for the button. | string | 'More info' |
hrefOptional Learn more link. | string | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- A button with aria-expanded; content is rendered in a polite live region when opened (toggletip pattern).
- Escape closes and keeps focus on the button.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-itp; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- Enter / Space
- Toggle
- Esc
- Close
FAQ
- Is Info Tip free?
- Yes. Info Tip is free for personal and commercial projects, with full source on this page.
- How do I install Info Tip?
- Run npx shadcn@latest add https://sundaeui.dev/r/info-tip.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Info Tip support keyboard and screen readers?
- A button with aria-expanded; content is rendered in a polite live region when opened (toggletip pattern). Escape closes and keeps focus on the button.
- Does Info Tip respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →Hover Card
Components · RefinedAvatar Stack
Components · Refined- Pro
Notification Center
Components · Refined Snooze Menu
Components · RefinedKeyboard Shortcuts Sheet
Components · Refined- Pro
Feature Tour
Components · Refined