- Ivy is a declarative full-stack UI framework that allows developers to build user interfaces using a component-based approach very similar to React.
- In Ivy, you only write one application in pure C# and we don't have a BE and FE distinction.
- UI rendering is handled by Ivy.
- When programming in Ivy you focus on building the logical structure of your application using a large set of pre-built widgets and views - you rarely need to specify any styling - Ivy just makes it look good by default.
Terminology:
- "Views" are the main building blocks of the UI, similar to React components.
- "Hooks" are functions that allow views to manage state and side effects.
- "Widgets" are the UI elements that make up the views. e.g., Button, TextBlock, StackPanel...
- "Apps" are the top-level views that represent entire applications.
A view is defined as a class that inherits from ViewBase and implements a Build method. The Build method returns either another view or a widget.
WARNING: There is NO
AppBaseclass. ALL views and apps inherit fromViewBase.
Widgets can have multiple children, but views can only return a single object (widget or view). To return multiple widgets from a view, you can use a Fragment use the Layout helpers. See below.
public class MyView : ViewBase { public override object? Build() { var count = UseState(0); return Layout.Vertical() | new Text($"Count: {count.Value}") | new Button("Increment", () => count.Set(count.Value + 1)); } }
The topmost view in an Ivy application is called an App and is decorated with the [App] attribute. The attribute uses named constructor parameters (lowercase):
[App(title: "Customers", icon: Icons.Rocket, group: new[] { "CRM" })] public class CustomersApp : ViewBase
titleis optional — if omitted, it is derived from the class name (e.g.CustomersApp→ "Customers").iconuses theIconsenum — these are Lucide icons in PascalCase (e.g.Icons.Link,Icons.Settings,Icons.Rocket).groupgroups the app in the navigation sidebar (e.g.group: new[] { "Apps" }).- There is no
chromeparameter. Chrome is configured inProgram.csviaserver.UseDefaultApp(typeof(MyApp)). Always ensureserver.AddAppsFromAssembly()is called beforeUseDefaultApp— without it,[App]-attributed classes are not registered and the server throws at runtime. - Use lowercase parameter names (
icon:,group:), NOT PascalCase property names (Icon =,Group =) — PascalCase causes CS0655.
An app is built into a tree of widgets. This is what's rendered to the screen.
A typical Ivy project has this folder structure:
MyProject/ ├── Program.cs # Entry point — configures and starts the Ivy server ├── MyProject.csproj # Project file ├── Apps/ # All app classes go here (convention) │ ├── DashboardApp.cs │ └── Settings/ # Subfolder namespaces become URL path segments │ └── UserProfileApp.cs # → /settings/user-profile └── Connections/ └── MyDb/ ├── MyDbContext.cs ├── MyDbContextFactory.cs ├── MyDbConnection.cs └── Product.cs # Entity classes
Register services in Program.cs, consume them in views with UseService<T>():
// Program.cs server.Services.AddSingleton<IMyService, MyService>();
// In a view var myService = UseService();
Button Card Badge Sheet Progress Expandable Tooltip DropDownMenu Table List Details Image Avatar Spacer Callout
- Use Layout.Vertical() or Layout.Horizontal() to create stack layouts.
- Layout.Grid() — Layout.Wrap()
- Add Children: Pipe child elements using the | operator to arrange them top-to-bottom (vertical) or left-to-right (horizontal).
- Layouts can be customized with methods like .Gap(int number) to set spacing between children. Use .Left(), .Center(), or .Right() methods to control alignment.
- The number in Gap(int number) works the same as in Tailwind CSS spacing scale (e.g., 1 = 0.25rem, 2 = 0.5rem, etc.).
- Layouts have a default gap of 4 (1rem). Do NOT add
.Gap(4)— it is the default and adds unnecessary noise. Only use.Gap()when you need a value other than 4. .Padding()is rarely needed. Layouts and pages already have appropriate padding by default. Only add.Padding()when you need extra inner spacing for a specific design reason.
// Basic Vertical Layout Layout.Vertical() | new Badge("Top") | new Badge("Middle") | new Badge("Bottom");
// Nested layouts with alignment Layout.Vertical().AlignContent(Align.Center) | Text.Label("Header") | (Layout.Horizontal() | new Button("Previous") | new Button("Next")); //NOTE: Parentheses are used to group the horizontal layout - THIS IS REQUIRED
Grids:
Layout.Grid() .Columns(2) .Rows(2) | Text.Block("Cell 1") | Text.Block("Cell 2") ...
Align values: TopLeft, TopCenter, TopRight, Left, Center, Right, BottomLeft, BottomCenter, BottomRight, Stretch
The Text helper utility is used to create various semantic text elements.
- Text.H1, Text.H2, : For headings.
- Text.Lead: For prominent introductory text.
- Text.P: For standard paragraphs.
- Text.Block: For block-level content (e.g., list items).
- Text.InlineCode: For displaying inline code snippets.
- Test.Muted
Styling Modifiers: .NoWrap(): .Bold() .Italic() .StrikeThrough() .Color(Colors)
Layout.Vertical() | Text.H1("Getting Started") | Text.P("This is a paragraph of text.").NoWrap()
Ivy.Colors enum has the following values:
Black, White, Slate, Gray, Zinc, Neutral, Stone, Red, Orange, Amber, Yellow, Lime, Green, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, Fuchsia, Pink, Rose, Primary, Secondary, Destructive, Success, Warning, Info, Muted
All widgets support .Density(Density.Small), .Density(Density.Medium), .Density(Density.Large).
Convenience methods: .Small(), .Medium(), .Large().
Density adjusts the overall visual size of a widget (text, padding, etc.).
There is no ButtonSize enum — use Density for all widgets.
.Width(Size.X) and .Height(Size.X) set widget dimensions.
.Size(Size.X) sets both width and height.
Common Size values:
- Size.Units(n) — Tailwind spacing scale (n × 0.25rem)
- Size.Full() — 100%
- Size.Fit() — fit-content
- Size.Auto() — auto
- Size.Px(n) — exact pixels
- Size.Fraction(0.5f) — percentage, Size.Half(), Size.Third()
Size is NOT the same as Density. Size controls dimensions; Density controls visual density.
new Button("Click Me") .Primary() .OnClick(() => { count.Set(count.Value + 1); })
new TextInput().Default() .Value(text.Value) .OnChange(text.Set) .OnBlur(() => Console.WriteLine("Blurred"))
- Most hooks that you know from React are available in Ivy. They follow the same principles as in React.
- Hooks should only be called at the top level of a Build() method, not inside loops or conditions.
var nameState = UseState("World"); var iconsState = UseState<Icons[]>();
If you don't specify a value, default(T) is used.
UseState hook returns a state object IState that provides:
WARNING: UseState returns
IState<T>, NOTState<T>. There is noState<T>type in Ivy.
- .Value property to read the current state.
- .Set(newValue) method to update the state in UseEffect or in an event handler.
Always use immutable types (e.g. records) with UseState — mutable classes that are modified in-place and passed back via .Set() will not trigger a re-render because the reference hasn't changed. Instead, create a new instance (e.g. using with expressions on records) before calling .Set().
void UseEffect(Action effect, params IEffectTriggers[] triggers) void UseEffect(Func asyncEffect, params IEffectTriggers[] triggers) void UseEffect(Func effectWithCleanup, params IEffectTriggers[] triggers) void UseEffect(Func<Task> asyncEffectWithCleanup, IEffectTriggers object[] triggers)
-
EffectTrigger.OnBuild() - runs after every build
-
EffectTrigger.OnMount() - runs once when the view is first mounted
-
EffectTrigger.OnStateChange(IState) - runs when the specified state changes
-
IState is automatically converted to EffectTrigger.OnStateChange
-
If no triggers are provided, the effect trigger is assumed to be OnMount.
UseQuery is the preferred pattern for data fetching in Ivy. It should be favored over the UseEffect + UseState fetch pattern.
var query = UseQuery( key: "my-data", fetcher: async (ct) => await LoadDataAsync(ct) );
if (query.Loading) return Skeleton.Card(); if (query.Error is { } error) return Callout.Error(error.Message);
// Use query.Value
QueryResult properties:
- .Value — the fetched data (default until loaded)
- .Loading — true during initial fetch (no value yet)
- .Validating — true during background revalidation
- .Error — exception if the fetch failed
- .Mutator — provides .Revalidate(), .Invalidate(), .Mutate(value, revalidate)
Do not combine UseQuery with DataTable for EF Core data. When a DataTable can receive an EF Core IQueryable directly, pass it to .ToDataTable() without UseQuery. DataTables handle their own server-side data loading. (API data fetched via UseQuery → .ToDataTable() is fine.)
Key conventions:
- String:
"my-data" - Tuple:
(nameof(MyBlade), entityId)
Common options (QueryOptions):
- KeepPrevious: true — show stale data while revalidating with a new key
- RevalidateOnMount: false — skip initial fetch when using initialValue
- RefreshInterval: TimeSpan — poll at an interval
- Scope: QueryScope.View — isolate cache to the view instance (default is Server)
Tag-based invalidation (cross-component): var queryService = UseService(); queryService.RevalidateByTag(typeof(Product[])); // collection queryService.RevalidateByTag((typeof(Product), id)); // single entity
Static "hooks" pattern (reusable across views): public static QueryResult<T[]> UseMyRecords(IViewContext context, string filter) { return context.UseQuery( key: (nameof(UseMyRecords), filter), fetcher: async ct => { /fetch/ }, tags: [typeof(T[])], options: new QueryOptions { KeepPrevious = true } ); }
Dependent fetching (wait for another query): var user = UseQuery(key: "user", fetcher: async ct => await GetUser(ct)); var projects = UseQuery( () => user.Value?.Id, // null = idle, no fetch async (userId, ct) => await GetProjects(userId, ct));
UseRef UseContext UseQuery UseMutation UseSignal UseService UseArgs UseDownload UseRefreshToken UseTrigger UseWebhook UseAlert
Ivy has several Input widgets for handling user input. There are rarely used directly - instead we use extension methods on IState to bind state to inputs.
var userNameState = UseState(""); var input = userNameState.ToTextInput().Placeholder("Enter your name");
Most inputs have extension methods for common configurations: userNameState.ToTextInput().Required().MaxLength(50).Placeholder("Enter your name");
TextInput NumberInput BoolInput SelectInput AsyncSelectInput DateTimeInput DateRangeInput ColorInput CodeInput FeedbackInput FileInput
- Base class is
ViewBase(NOTAppBasethere is noAppBaseclass) Textis a static helper - useText.P(),Text.H2(), ...UseState<T>()returnsIState<T>, NOTState<T>- All types are in the
Ivynamespace Colorsis a flat enum (e.g.Colors.Red,Colors.Blue) we have no shade levelsDbContextmust never be injected directly! Always resolveIDbContextFactory<T>viaUseServiceand create scoped instances withCreateDbContextAsync()inside query/mutation lambdas- Nested layouts MUST use parentheses —
Layout.Vertical() | (Layout.Horizontal() | child1 | child2)— without parentheses, C# left-to-right|evaluation adds children to the outer layout, not the inner one. See the nested layout example in the Layouts section above.
Prefer using ivy cli explain for command discovery over MCP server tools as it provides a reliable, built-in structural breakdown.
Forms DataTable Table Details - Display structured label-value pairs Program.cs Size Align Downloads Icons
All Ivy documentation pages are listed on: https://docs.ivy.app/sitemap.xml. Add ".md" to the end of any URL to go directly to the Markdown version of the doc.