happenings_ui 0.2.2
happenings_ui: ^0.2.2 copied to clipboard
Happenings design system — tokens, theme, and shared components for Flutter apps built on Material 3 with a Zinc color palette.
happenings_ui #
A design system for Flutter, built on the belief that good UI is felt before it is seen.
Philosophy #
happenings_ui draws deep inspiration from shadcn/ui — the idea that components should be beautiful by default, composable by nature, and yours to own. We admire what shadcn did for the React ecosystem: proving that a design system doesn't need to be a black box. It can be a set of well-considered defaults that you understand, trust, and build on top of.
We carry that ethos into Flutter.
Zinc as a foundation #
Color sets the emotional tone of an interface before a single word is read. We chose the Zinc neutral palette — ported from Tailwind CSS — as our foundation. Zinc is cooler than Gray, warmer than Slate. It has just enough personality to feel intentional without competing with content. Every surface, border, and text shade in the system traces back to the Zinc scale, from zinc50 to zinc950.
Brand colors — Indigo, Green, Blue — are accents, not backgrounds. They earn their presence by being used sparingly: a primary action, a success state, a navigational hint. The UI breathes in Zinc; color arrives only when it has something to say.
The 4px spatial rhythm #
Spacing is the invisible architecture of an interface. When spacing is inconsistent, layouts feel uneasy even if no one can articulate why. When spacing follows a rhythm, everything clicks into place.
We use a 4px base unit — a spatial scale where every value is a purposeful multiple of 4: 4, 8, 12, 16, 20, 24, 32. This creates a vertical and horizontal rhythm that the eye naturally trusts. Elements feel like they belong together, sections feel distinct, and the whole screen reads as one composed thought rather than a collection of parts.
Typography with intent #
We set type in Geist — a typeface built for interfaces. Clean, highly legible at small sizes, distinctive at large ones. The type scale is deliberately tight: page titles, section headings, card titles, body, caption, overline. Six roles. No ambiguity about which to use.
Tight negative letter-spacing on headings creates density and confidence. Generous line heights on body text create breathing room for reading. The contrast between the two is what gives a screen its visual hierarchy.
Scale-down, not ripple #
Material Design's ink ripple is an answer to a question we're not asking. A ripple says "you touched here" — it's positional feedback for a mouse-driven world. On a phone, your finger already knows where it pressed. What it doesn't know is whether the thing it pressed responded.
We replaced ripple entirely with a scale-down animation. When you press a button, it compresses to 0.97x its size. When you release, it springs back. The whole surface moves as one — not a circle expanding from a point, but the element itself breathing under your thumb. The default duration is 100ms with an easeOut curve in both directions: fast enough to feel instant, slow enough to be perceived. No delay, no overshoot, no bounce. Just a clean compression and release that says "I heard you."
The scale factor is tunable per component. Navigation tabs press deeper (0.92x) for a more tactile feel. Cards barely yield (0.97x) because they're surfaces, not buttons. The swipe-to-confirm thumb scales on completion. Every interactive element participates in the same physical language, but each speaks at its own volume.
This is HappeningsPressable — the primitive that every interactive component in the system is built on. It's a GestureDetector driving an AnimationController driving a ScaleTransition. No InkWell, no Material widget, no splash factory. Just physics.
Skeleton loading #
An empty screen is a broken promise. A spinner is a vague one. A skeleton — a shimmer placeholder shaped like the content that's coming — is a specific promise: "this is what you're about to see, and it's almost here."
HappeningsShimmer renders rounded rectangles in Zinc tones with a horizontal shimmer sweep. In light mode, the base is zinc200 and the highlight sweeps through zinc100. In dark mode, zinc800 and zinc700. The shimmer doesn't fight the theme — it is the theme, just moving. Default border radius is 8px to match cards and inputs, but it's configurable for avatars (circular) or badges (tighter radius).
The philosophy is simple: skeleton screens should be boring. They should look like the real content with the color drained out. When the real data arrives and replaces the shimmer, the transition should feel like a curtain lifting — not a scene change. That's why we match the exact dimensions and border radii of the final components.
Haptic feedback #
Touch is not a single sense. There's the visual response (the scale-down), and then there's the physical one — the tiny vibration in the glass that confirms an action landed. We treat haptic feedback as a first-class design token, not an afterthought.
Four intensity levels, each mapped to a semantic role:
- Light — the default. Every standard button press, every card tap. A whisper that says "registered."
- Medium — destructive actions, confirmations, swipe-to-confirm completions. Enough force to make you pause for a fraction of a second. Intentional friction.
- Heavy — reserved. Major, irreversible moments. Rarely used, so when it fires, you notice.
- Selection — a crisp tick for toggles, pickers, and navigation tab switches. Different character from impact — more like a detent on a physical dial than a tap on a surface.
Haptics are globally toggleable via HappeningsUiConfig(hapticsEnabled: false) — accessibility matters, and not everyone wants vibration. But when enabled, they complete the feedback loop that starts with the scale-down animation. Press a button: it shrinks under your finger, the phone taps back, and the action fires. Three channels of feedback — visual, tactile, functional — arriving in the same 100ms window. That's what makes an interface feel solid.
Composability over configuration #
Components are designed to be composed, not configured into oblivion. A HappeningsButton does one thing well. A HappeningsSheet provides a surface; what goes inside is yours. We avoid prop sprawl — the kind of API where a component has forty parameters and none of them do quite what you need.
When you need something the system doesn't provide, build it from the same tokens. The spacing scale, the color palette, the typography — these are the shared language. The components are just the most common sentences.
Quick start #
Wrap your app with HappeningsUiConfig and apply the theme:
import 'package:happenings_ui/happenings_ui.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HappeningsUiConfig(
child: MaterialApp(
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
home: const HomeScreen(),
),
);
}
}
Custom icon library #
The default icon renderer uses Material Icons. To use Font Awesome or another library, pass a custom iconBuilder and icons:
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
HappeningsUiConfig(
iconBuilder: (icon, {size, color}) => FaIcon(icon, size: size, color: color),
icons: const HappeningsIcons(
back: FontAwesomeIcons.chevronLeft,
close: FontAwesomeIcons.xmark,
check: FontAwesomeIcons.check,
// ...
),
child: MaterialApp(...),
)
Haptics #
Disable haptic feedback globally:
HappeningsUiConfig(
hapticsEnabled: false,
child: MaterialApp(...),
)
Localised strings #
Override default labels for components that display text:
HappeningsUiConfig(
strings: HappeningsStrings(
cancel: l10n.cancel,
continueLabel: l10n.continueButton,
copy: l10n.copy,
),
child: MaterialApp(...),
)
Components #
Interaction primitives #
HappeningsPressable — The foundation of every interactive element. Replaces Material's ripple effect with a physical scale-down animation (0.97x, 100ms ease-out) and configurable haptic feedback. Every button, card tap, and navigation item builds on this. It's the reason the whole system feels cohesive when you touch it.
HappeningsButton — Three variants: primary (solid Zinc fill), secondary (outlined), and ghost (text-only). Supports a leading icon, a loading state that swaps the label for a shimmer pill, and disabled opacity. Built on HappeningsPressable, so every tap has weight.
SwipeToConfirmButton — A horizontal slider that requires dragging a thumb to 70% of the track to confirm a destructive action. The label text fades as the thumb moves; a checkmark scales in on confirmation. If the async callback fails, the thumb rolls back. Two visual variants (dark/light) adapt to different surface colors. Deliberate friction for irreversible moments.
Form inputs #
HappeningsInput — Text field with a rounded border (8px radius) and an animated focus ring that blooms outward (3px spread, 150ms). Supports label, hint, error state (red ring), and prefix/suffix icons. No ripple, no splash — just a clean border animation that tells you where you are.
HappeningsTextarea — Multi-line sibling to HappeningsInput with the same focus-ring and error treatment. Auto-grows up to a configurable max line count; shows a character counter in caption style when limits are set.
HappeningsSelect — Dropdown trigger styled identically to HappeningsInput for visual consistency. Uses a lightweight MenuAnchor popover (not a bottom sheet), with a checkmark on the selected item. Keeps the user in context instead of hijacking the screen.
HappeningsOtpInput — Six individual digit slots with 14px rounded corners, grouped in threes with a dash separator. Each digit pops in with an easeOutBack animation (200ms). A blinking caret marks the active slot. Backspace moves focus backwards. Supports autofill.
Layout & surfaces #
HappeningsCard — A container with a 1px Zinc border, 12px rounded corners, and 16px internal padding. Optionally tappable (wraps itself in HappeningsPressable when an onTap is provided). Simple, versatile, and the most-used surface in the system.
HappeningsSheet — Modal bottom sheet with a rounded header (30px radius), drag handle, optional icon/title/subtitle header, a scrollable body, and an optional sticky footer. Capped at 80% screen height with bouncing scroll physics. Includes built-in variants for confirmation dialogs, selection lists, and action menus.
HappeningsAppBar — Cupertino-inspired app bar with a pinned navigation overlay and an optional collapsing large title (32px bold). Applies an adaptive backdrop blur based on scroll position — fully transparent at the top, frosted glass as you scroll. Supports pull-to-refresh via CupertinoSliverRefreshControl.
HappeningsBottomNavBar — Bottom navigation with two visual styles: elevated (frosted glass background, highlighted selected tab) and iOS-clean (translucent, color-tinted). Each tab animates between active/inactive icon variants with selection haptics. Scale-down on press (0.92x elevated, 0.96x iOS) gives each tap a tactile punch.
CupertinoSearchAppBar — iOS-style search bar that collapses into the navigation bar on scroll. Integrates a debounced search field with a staggered large-title layout. Built with custom slivers and CompositedTransformTarget to anchor overlays like MentionOverlay precisely.
Feedback & state #
HappeningsToast — Slides down from the top of the screen (400ms ease-out, 250ms reverse). Supports icon, title, optional description, and an action button. Auto-dismisses after a configurable duration; swipe up to dismiss manually. Subtle elevation shadow and semi-transparent border.
HappeningsStateCard — Persistent inline alert with four severity levels: info (blue), warning (amber), error (red), success (green). Shows an icon in a tinted tile alongside a title, optional description, optional action, and optional dismiss button. Used for known states and blockers — not transient notifications.
HappeningsEmptyState — Centered placeholder with a stacked layout: large muted icon, title, optional subtitle, optional call-to-action button. The go-to component for zero-data screens and first-run states.
HappeningsShimmer — Skeleton loading placeholder with a shimmer animation. Supports fixed or flexible dimensions and customizable border radius (default 8px). Theme-aware base and highlight colors ensure it looks right on both light and dark surfaces.
Content & media #
HappeningsAvatar — Circular avatar with cached network image, fallback to two-letter initials (derived from first + last name), or a generic user icon. A 1px Zinc border gives it definition against any background. Responsive sizing.
HappeningsBadge — Small label chip with 6px rounded corners and Zinc-themed defaults. Used for status tags, category labels, and counters. Intentionally minimal — color and text, nothing more.
HappeningsNetworkImage — Cached image loader with a Zinc-styled shimmer placeholder during load and a tap-to-retry error state. Custom 10-second timeout per request; evicts cache and retries on tap.
HappeningsMarkdown — Renders GitHub-flavored markdown with Zinc typography. Custom link handling for mention: protocol (styled blue, non-navigable); regular links open externally. Supports code blocks, blockquotes, tables, and images (safe URLs only, with error retry). Compact and normal density modes.
Social & onboarding #
MentionOverlay — Autocomplete popup triggered by @ in a text field. Shows a filtered list of users (avatar, name, subtitle) in a 200px-max overlay positioned above the cursor. Selecting a user inserts @name into the text. Typing continuously filters by display name.
MutualFriendChips — Overlapping circular avatars (Instagram-style stacking) with white/Zinc borders. Shows up to three faces with a "+N" overflow badge. Each avatar is independently tappable. A compact way to show social proof and shared connections.
HappeningsOnboarding — Paged onboarding flow with PageView, animated dot indicator, and a bottom action button. Supports collapsing large titles on scroll and configurable page transitions (default easeOutBack). Includes structured page types: simple title+body layouts and feature-list layouts with icon rows.
AnimatedBubbles — Six floating white circles that pulse with staggered scale and opacity animations on 3–5 second cycles. A purely decorative background layer — randomized positions, gentle easing, no interaction. Used on wallet cards to create depth and visual warmth.
HappeningsRefreshIndicator — Zinc-themed wrapper around Material's RefreshIndicator. Matches the spinner and background colors to the design system theme. Drop-in replacement for standard pull-to-refresh.
Tokens #
Colors #
// Zinc neutrals — the backbone of every surface
ZincColors.zinc50 // lightest
ZincColors.zinc950 // darkest
// Brand accents — used sparingly, with purpose
BrandColors.indigo // primary brand
BrandColors.green // success / positive
BrandColors.blue // info / links
Spacing #
// 4px base unit — every value a deliberate step in the rhythm
HapSpacing.xs // 4 — tight (icon gaps, inline elements)
HapSpacing.sm // 8 — small (between related elements)
HapSpacing.md // 12 — medium (input padding, card gaps)
HapSpacing.lg // 16 — default (card padding, section gaps)
HapSpacing.xl // 20 — large (sheet body padding)
HapSpacing.xxl // 24 — extra-large (page padding, empty states)
HapSpacing.xxxl // 32 — section-level spacing
Typography #
AppTypography.pageTitle() // 32px, bold — page-level headings
AppTypography.sectionHeading() // 20px, bold — within-page sections
AppTypography.cardTitle() // 16px, bold — card-level headings
AppTypography.body() // 14px, regular — default body text
AppTypography.bodyMedium() // 14px, medium — emphasized body text
AppTypography.caption() // 12px, medium — labels and captions
AppTypography.overline() // 11px, medium — categories and tags
Requirements #
- Flutter 3.32+
- Dart SDK 3.11+